initial commit
This commit is contained in:
668
LICENSE
Normal file
668
LICENSE
Normal file
@@ -0,0 +1,668 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
31
README.md
Normal file
31
README.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# KM Hershey Text
|
||||||
|
|
||||||
|
An Inkscape extension for rendering text using single-stroke (engraving) fonts. Designed for pen plotters, laser engravers, and CNC machines where stroke-based fonts produce cleaner results than filled outline fonts. Basically like the built-in hershey text extension but with a generate text option and a few more fonts.
|
||||||
|
|
||||||
|
|
||||||
|
## Manual Installation
|
||||||
|
|
||||||
|
1. Create the subdirectory `km-hershey/` in your Inkscape extensions folder:
|
||||||
|
- **Linux:** `~/.config/inkscape/extensions/`
|
||||||
|
- **Linux (Flatpak):** `~/.var/app/org.inkscape.Inkscape/config/inkscape/extensions/`
|
||||||
|
- **Linux (Snap):** `~/snap/inkscape/current/.config/inkscape/extensions/`
|
||||||
|
- **macOS:** `~/Library/Application Support/org.inkscape.Inkscape/config/inkscape/extensions/`
|
||||||
|
- **Windows:** `%APPDATA%\inkscape\extensions\`
|
||||||
|
2. Copy all files from this repository into your `km-hershey/` directory.
|
||||||
|
3. Restart Inkscape. The extension appears under **Extensions > Knox Makers > Laser > Hershey Text**.
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
Inspiration, examples, and code came from:
|
||||||
|
|
||||||
|
The now Inkscape built-in hershey text extension
|
||||||
|
https://gitlab.com/inkscape/extensions/-/blob/master/hershey.py
|
||||||
|
|
||||||
|
Which originally came from from Evil Mad Scientist Laboratories
|
||||||
|
https://github.com/evil-mad/
|
||||||
|
|
||||||
|
Based on Hershey fonts created for plotters
|
||||||
|
https://en.wikipedia.org/wiki/Hershey_fonts
|
||||||
|
|
||||||
|
Cutlings
|
||||||
|
https://cutlings.datafil.no/
|
||||||
33
deps/inkex/__init__.py
vendored
Normal file
33
deps/inkex/__init__.py
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
"""
|
||||||
|
This describes the core API for the inkex core modules.
|
||||||
|
|
||||||
|
This provides the basis from which you can develop your inkscape extension.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=wildcard-import
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from .extensions import *
|
||||||
|
from .utils import AbortExtension, DependencyError, Boolean, errormsg
|
||||||
|
from .styles import *
|
||||||
|
from .paths import Path, CubicSuperPath # Path commands are not exported
|
||||||
|
from .colors import Color, ColorError, ColorIdError, is_color
|
||||||
|
from .colors.spaces import *
|
||||||
|
from .transforms import *
|
||||||
|
from .elements import *
|
||||||
|
|
||||||
|
# legacy proxies
|
||||||
|
from .deprecated import Effect
|
||||||
|
from .deprecated import localize
|
||||||
|
from .deprecated import debug
|
||||||
|
|
||||||
|
# legacy functions
|
||||||
|
from .deprecated import are_near_relative
|
||||||
|
from .deprecated import unittouu
|
||||||
|
|
||||||
|
MIN_VERSION = (3, 7)
|
||||||
|
if sys.version_info < MIN_VERSION:
|
||||||
|
sys.exit("Inkscape extensions require Python 3.7 or greater.")
|
||||||
|
|
||||||
|
__version__ = "1.4.0" # Version number for inkex; may differ from Inkscape version.
|
||||||
BIN
deps/inkex/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/base.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/base.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/base.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/base.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/bezier.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/bezier.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/bezier.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/bezier.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/command.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/command.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/command.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/command.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/extensions.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/extensions.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/extensions.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/extensions.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/localization.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/localization.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/localization.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/localization.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/properties.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/properties.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/properties.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/properties.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/styles.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/styles.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/styles.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/styles.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/transforms.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/transforms.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/transforms.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/transforms.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/units.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/units.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/units.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/units.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/utils.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/utils.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/utils.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/utils.cpython-313.pyc
vendored
Normal file
Binary file not shown.
567
deps/inkex/base.py
vendored
Normal file
567
deps/inkex/base.py
vendored
Normal file
@@ -0,0 +1,567 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 - Martin Owens <doctormo@gmail.com>
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
The ultimate base functionality for every Inkscape extension.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from typing import (
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
Tuple,
|
||||||
|
Type,
|
||||||
|
Optional,
|
||||||
|
Callable,
|
||||||
|
Any,
|
||||||
|
Union,
|
||||||
|
IO,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
|
||||||
|
from argparse import ArgumentParser, Namespace
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
from .utils import filename_arg, AbortExtension, ABORT_STATUS, errormsg, do_nothing
|
||||||
|
from .elements._parser import load_svg
|
||||||
|
from .elements._utils import NSS
|
||||||
|
from .localization import localize
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .elements._svg import SvgDocumentElement
|
||||||
|
from .elements._base import BaseElement
|
||||||
|
|
||||||
|
|
||||||
|
class InkscapeExtension:
|
||||||
|
"""
|
||||||
|
The base class extension, provides argument parsing and basic
|
||||||
|
variable handling features.
|
||||||
|
"""
|
||||||
|
|
||||||
|
multi_inx = False # Set to true if this class is used by multiple inx files.
|
||||||
|
extra_nss = {} # type: Dict[str, str]
|
||||||
|
|
||||||
|
# Provide a unique value to allow detection of no argument specified
|
||||||
|
# for `output` parameter of `run()`, not even `None`; this has to be an io
|
||||||
|
# type for type checking purposes:
|
||||||
|
output_unspecified = io.StringIO("")
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# type: () -> None
|
||||||
|
NSS.update(self.extra_nss)
|
||||||
|
self.file_io = None # type: Optional[IO]
|
||||||
|
self.options = Namespace()
|
||||||
|
self.document = None # type: Union[None, bytes, str, etree.element]
|
||||||
|
self.arg_parser = ArgumentParser(description=self.__doc__)
|
||||||
|
|
||||||
|
self.arg_parser.add_argument(
|
||||||
|
"input_file",
|
||||||
|
nargs="?",
|
||||||
|
metavar="INPUT_FILE",
|
||||||
|
type=filename_arg,
|
||||||
|
help="Filename of the input file (default is stdin)",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.arg_parser.add_argument(
|
||||||
|
"--output",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help="Optional output filename for saving the result (default is stdout).",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.add_arguments(self.arg_parser)
|
||||||
|
|
||||||
|
localize()
|
||||||
|
|
||||||
|
def add_arguments(self, pars):
|
||||||
|
# type: (ArgumentParser) -> None
|
||||||
|
"""Add any extra arguments to your extension handle, use:
|
||||||
|
|
||||||
|
def add_arguments(self, pars):
|
||||||
|
pars.add_argument("--num-cool-things", type=int, default=3)
|
||||||
|
pars.add_argument("--pos-in-doc", type=str, default="doobry")
|
||||||
|
"""
|
||||||
|
# No extra arguments by default so super is not required
|
||||||
|
|
||||||
|
def parse_arguments(self, args):
|
||||||
|
# type: (List[str]) -> None
|
||||||
|
"""Parse the given arguments and set 'self.options'"""
|
||||||
|
self.options = self.arg_parser.parse_args(args)
|
||||||
|
|
||||||
|
def arg_method(self, prefix="method"):
|
||||||
|
# type: (str) -> Callable[[str], Callable[[Any], Any]]
|
||||||
|
"""Used by add_argument to match a tab selection with an object method
|
||||||
|
|
||||||
|
pars.add_argument("--tab", type=self.arg_method(), default="foo")
|
||||||
|
...
|
||||||
|
self.options.tab(arguments)
|
||||||
|
...
|
||||||
|
.. code-block:: python
|
||||||
|
.. def method_foo(self, arguments):
|
||||||
|
.. # do something
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _inner(value):
|
||||||
|
name = f"""{prefix}_{value.strip('"').lower()}""".replace("-", "_")
|
||||||
|
try:
|
||||||
|
return getattr(self, name)
|
||||||
|
except AttributeError as error:
|
||||||
|
if name.startswith("_"):
|
||||||
|
return do_nothing
|
||||||
|
raise AbortExtension(f"Can not find method {name}") from error
|
||||||
|
|
||||||
|
return _inner
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def arg_number_ranges():
|
||||||
|
"""Parses a number descriptor. e.g:
|
||||||
|
``1,2,4-5,7,9-`` is parsed to ``1, 2, 4, 5, 7, 9, 10, ..., lastvalue``
|
||||||
|
|
||||||
|
.. versionadded:: 1.2
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# in add_arguments()
|
||||||
|
pars.add_argument("--pages", type=self.arg_number_ranges(), default=1-)
|
||||||
|
# later on, pages is then a list of ints
|
||||||
|
pages = self.options.pages(lastvalue)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _inner(value):
|
||||||
|
def method(pages, lastvalue, startvalue=1):
|
||||||
|
# replace ranges, such as -3, 10- with startvalue,2,3,10..lastvalue
|
||||||
|
pages = re.sub(
|
||||||
|
r"(\d+|)\s?-\s?(\d+|)",
|
||||||
|
lambda m: (
|
||||||
|
",".join(
|
||||||
|
map(
|
||||||
|
str,
|
||||||
|
range(
|
||||||
|
int(m.group(1) or startvalue),
|
||||||
|
int(m.group(2) or lastvalue) + 1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not (m.group(1) or m.group(2)) == ""
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
pages,
|
||||||
|
)
|
||||||
|
|
||||||
|
pages = map(int, re.findall(r"(\d+)", pages))
|
||||||
|
pages = tuple({i for i in pages if i <= lastvalue})
|
||||||
|
return pages
|
||||||
|
|
||||||
|
return lambda lastvalue, startvalue=1: method(
|
||||||
|
value, lastvalue, startvalue=startvalue
|
||||||
|
)
|
||||||
|
|
||||||
|
return _inner
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def arg_class(options: List[Type]) -> Callable[[str], Any]:
|
||||||
|
"""Used by add_argument to match an option with a class
|
||||||
|
|
||||||
|
Types to choose from are given by the options list
|
||||||
|
|
||||||
|
.. versionadded:: 1.2
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
pars.add_argument("--class", type=self.arg_class([ClassA, ClassB]),
|
||||||
|
default="ClassA")
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _inner(value: str):
|
||||||
|
name = value.strip('"')
|
||||||
|
for i in options:
|
||||||
|
if name == i.__name__:
|
||||||
|
return i
|
||||||
|
raise AbortExtension(f"Can not find class {name}")
|
||||||
|
|
||||||
|
return _inner
|
||||||
|
|
||||||
|
def debug(self, msg):
|
||||||
|
# type: (str) -> None
|
||||||
|
"""Write a debug message"""
|
||||||
|
errormsg(f"DEBUG<{type(self).__name__}> {msg}\n")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def msg(msg):
|
||||||
|
# type: (str) -> None
|
||||||
|
"""Write a non-error message"""
|
||||||
|
errormsg(msg)
|
||||||
|
|
||||||
|
def run(self, args=None, output=output_unspecified):
|
||||||
|
# type: (Optional[List[str]], Union[str, IO]) -> None
|
||||||
|
"""Main entrypoint for any Inkscape Extension"""
|
||||||
|
try:
|
||||||
|
if args is None:
|
||||||
|
args = sys.argv[1:]
|
||||||
|
|
||||||
|
self.parse_arguments(args)
|
||||||
|
if self.options.input_file is None:
|
||||||
|
self.options.input_file = sys.stdin
|
||||||
|
elif "DOCUMENT_PATH" not in os.environ:
|
||||||
|
os.environ["DOCUMENT_PATH"] = self.options.input_file
|
||||||
|
|
||||||
|
self.bin_stdout = None
|
||||||
|
if self.options.output is None:
|
||||||
|
# If no output was specified, attempt to extract a binary
|
||||||
|
# output from stdout, and if that doesn't seem possible,
|
||||||
|
# punt and try whatever stream stdout is:
|
||||||
|
if output is InkscapeExtension.output_unspecified:
|
||||||
|
output = sys.stdout
|
||||||
|
if "b" not in getattr(output, "mode", "") and not isinstance(
|
||||||
|
output, (io.RawIOBase, io.BufferedIOBase)
|
||||||
|
):
|
||||||
|
if hasattr(output, "buffer"):
|
||||||
|
output = output.buffer # type: ignore
|
||||||
|
elif hasattr(output, "fileno"):
|
||||||
|
self.bin_stdout = os.fdopen(
|
||||||
|
output.fileno(), "wb", closefd=False
|
||||||
|
)
|
||||||
|
output = self.bin_stdout
|
||||||
|
self.options.output = output
|
||||||
|
|
||||||
|
self.load_raw()
|
||||||
|
self.save_raw(self.effect())
|
||||||
|
except AbortExtension as err:
|
||||||
|
errormsg(str(err))
|
||||||
|
sys.exit(ABORT_STATUS)
|
||||||
|
finally:
|
||||||
|
self.clean_up()
|
||||||
|
|
||||||
|
def load_raw(self):
|
||||||
|
# type: () -> None
|
||||||
|
"""Load the input stream or filename, save everything to self"""
|
||||||
|
if isinstance(self.options.input_file, str):
|
||||||
|
# pylint: disable=consider-using-with
|
||||||
|
self.file_io = open(self.options.input_file, "rb")
|
||||||
|
document = self.load(self.file_io)
|
||||||
|
else:
|
||||||
|
document = self.load(self.options.input_file)
|
||||||
|
self.document = document
|
||||||
|
|
||||||
|
def save_raw(self, ret):
|
||||||
|
# type: (Any) -> None
|
||||||
|
"""Save to the output stream, use everything from self"""
|
||||||
|
if self.has_changed(ret):
|
||||||
|
if isinstance(self.options.output, str):
|
||||||
|
with open(self.options.output, "wb") as stream:
|
||||||
|
self.save(stream)
|
||||||
|
else:
|
||||||
|
if sys.platform == "win32" and not "PYTEST_CURRENT_TEST" in os.environ:
|
||||||
|
# When calling an extension from within Inkscape on Windows,
|
||||||
|
# Python thinks that the output stream is seekable
|
||||||
|
# (https://gitlab.com/inkscape/inkscape/-/issues/3273)
|
||||||
|
self.options.output.seekable = lambda self: False
|
||||||
|
|
||||||
|
def seek_replacement(offset: int, whence: int = 0):
|
||||||
|
raise AttributeError(
|
||||||
|
"We can't seek in the stream passed by Inkscape on Windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
def tell_replacement():
|
||||||
|
raise AttributeError(
|
||||||
|
"We can't tell in the stream passed by Inkscape on Windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Some libraries (e.g. ZipFile) don't query seekable, but check for an error
|
||||||
|
# on seek
|
||||||
|
self.options.output.seek = seek_replacement
|
||||||
|
self.options.output.tell = tell_replacement
|
||||||
|
self.save(self.options.output)
|
||||||
|
|
||||||
|
def load(self, stream):
|
||||||
|
# type: (IO) -> str
|
||||||
|
"""Takes the input stream and creates a document for parsing"""
|
||||||
|
raise NotImplementedError(f"No input handle for {self.name}")
|
||||||
|
|
||||||
|
def save(self, stream):
|
||||||
|
# type: (IO) -> None
|
||||||
|
"""Save the given document to the output file"""
|
||||||
|
raise NotImplementedError(f"No output handle for {self.name}")
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
# type: () -> Any
|
||||||
|
"""Apply some effects on the document or local context"""
|
||||||
|
raise NotImplementedError(f"No effect handle for {self.name}")
|
||||||
|
|
||||||
|
def has_changed(self, ret): # pylint: disable=no-self-use
|
||||||
|
# type: (Any) -> bool
|
||||||
|
"""Return true if the output should be saved"""
|
||||||
|
return ret is not False
|
||||||
|
|
||||||
|
def clean_up(self):
|
||||||
|
# type: () -> None
|
||||||
|
"""Clean up any open handles and other items"""
|
||||||
|
if hasattr(self, "bin_stdout"):
|
||||||
|
if self.bin_stdout is not None:
|
||||||
|
self.bin_stdout.close()
|
||||||
|
if self.file_io is not None:
|
||||||
|
self.file_io.close()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def svg_path(cls, default=None):
|
||||||
|
# type: (Optional[str]) -> Optional[str]
|
||||||
|
"""
|
||||||
|
Return the folder the svg is contained in.
|
||||||
|
Returns None if there is no file.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.1
|
||||||
|
A default path can be given which is returned in case no path to the
|
||||||
|
SVG file can be determined.
|
||||||
|
"""
|
||||||
|
path = cls.document_path()
|
||||||
|
if path:
|
||||||
|
return os.path.dirname(path)
|
||||||
|
if default:
|
||||||
|
return default
|
||||||
|
return path # Return None or '' for context
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def ext_path(cls):
|
||||||
|
# type: () -> str
|
||||||
|
"""Return the folder the extension script is in"""
|
||||||
|
return os.path.dirname(sys.modules[cls.__module__].__file__ or "")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_resource(cls, name, abort_on_fail=True):
|
||||||
|
# type: (str, bool) -> str
|
||||||
|
"""Return the full filename of the resource in the extension's dir
|
||||||
|
|
||||||
|
.. versionadded:: 1.1"""
|
||||||
|
filename = cls.absolute_href(name, cwd=cls.ext_path())
|
||||||
|
if abort_on_fail and not os.path.isfile(filename):
|
||||||
|
raise AbortExtension(f"Could not find resource file: {filename}")
|
||||||
|
return filename
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def document_path(cls):
|
||||||
|
# type: () -> Optional[str]
|
||||||
|
"""Returns the saved location of the document
|
||||||
|
|
||||||
|
* Normal return is a string containing the saved location
|
||||||
|
* Empty string means the document was never saved
|
||||||
|
* 'None' means this version of Inkscape doesn't support DOCUMENT_PATH
|
||||||
|
|
||||||
|
DO NOT READ OR WRITE TO THE DOCUMENT FILENAME!
|
||||||
|
|
||||||
|
* Inkscape may have not written the latest changes, leaving you reading old
|
||||||
|
data.
|
||||||
|
* Inkscape will not respect anything you write to the file, causing data loss.
|
||||||
|
|
||||||
|
.. versionadded:: 1.1
|
||||||
|
"""
|
||||||
|
return os.environ.get("DOCUMENT_PATH", None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def absolute_href(cls, filename, default="~/", cwd=None):
|
||||||
|
# type: (str, str, Optional[str]) -> str
|
||||||
|
"""
|
||||||
|
Process the filename such that it's turned into an absolute filename
|
||||||
|
with the working directory being the directory of the loaded svg.
|
||||||
|
|
||||||
|
User's home folder is also resolved. So '~/a.png` will be `/home/bob/a.png`
|
||||||
|
|
||||||
|
Default is a fallback working directory to use if the svg's filename is not
|
||||||
|
available.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.1
|
||||||
|
If you set default to None, then the user will be given errors if
|
||||||
|
there's no working directory available from Inkscape.
|
||||||
|
"""
|
||||||
|
filename = os.path.expanduser(filename)
|
||||||
|
if not os.path.isabs(filename):
|
||||||
|
filename = os.path.expanduser(filename)
|
||||||
|
if not os.path.isabs(filename):
|
||||||
|
if cwd is None:
|
||||||
|
cwd = cls.svg_path(default)
|
||||||
|
if cwd is None:
|
||||||
|
raise AbortExtension(
|
||||||
|
"Can not use relative path, Inkscape isn't telling us the "
|
||||||
|
"current working directory."
|
||||||
|
)
|
||||||
|
if cwd == "":
|
||||||
|
raise AbortExtension(
|
||||||
|
"The SVG must be saved before you can use relative paths."
|
||||||
|
)
|
||||||
|
filename = os.path.join(cwd, filename)
|
||||||
|
return os.path.realpath(os.path.expanduser(filename))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
# type: () -> str
|
||||||
|
"""Return a fixed name for this extension"""
|
||||||
|
return type(self).__name__
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
_Base = InkscapeExtension
|
||||||
|
else:
|
||||||
|
_Base = object
|
||||||
|
|
||||||
|
|
||||||
|
class TempDirMixin(_Base): # pylint: disable=abstract-method
|
||||||
|
"""
|
||||||
|
Provide a temporary directory for extensions to stash files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dir_suffix = ""
|
||||||
|
dir_prefix = "inktmp"
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.tempdir = None
|
||||||
|
self._tempdir = None
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def load_raw(self):
|
||||||
|
# type: () -> None
|
||||||
|
"""Create the temporary directory"""
|
||||||
|
# pylint: disable=import-outside-toplevel
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
# Need to hold a reference to the Directory object or else it might get GC'd
|
||||||
|
self._tempdir = TemporaryDirectory( # pylint: disable=consider-using-with
|
||||||
|
prefix=self.dir_prefix, suffix=self.dir_suffix
|
||||||
|
)
|
||||||
|
self.tempdir = os.path.realpath(self._tempdir.name)
|
||||||
|
super().load_raw()
|
||||||
|
|
||||||
|
def clean_up(self):
|
||||||
|
# type: () -> None
|
||||||
|
"""Delete the temporary directory"""
|
||||||
|
self.tempdir = None
|
||||||
|
# if the file does not exist, _tempdir is never set.
|
||||||
|
if self._tempdir is not None:
|
||||||
|
self._tempdir.cleanup()
|
||||||
|
super().clean_up()
|
||||||
|
|
||||||
|
|
||||||
|
class SvgInputMixin(_Base): # pylint: disable=too-few-public-methods, abstract-method
|
||||||
|
"""
|
||||||
|
Expects the file input to be an svg document and will parse it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Select all objects if none are selected
|
||||||
|
select_all: Tuple[Type["BaseElement"], ...] = ()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.arg_parser.add_argument(
|
||||||
|
"--id",
|
||||||
|
action="append",
|
||||||
|
type=str,
|
||||||
|
dest="ids",
|
||||||
|
default=[],
|
||||||
|
help="id attribute of object to manipulate",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.arg_parser.add_argument(
|
||||||
|
"--selected-nodes",
|
||||||
|
action="append",
|
||||||
|
type=str,
|
||||||
|
dest="selected_nodes",
|
||||||
|
default=[],
|
||||||
|
help="id:subpath:position of selected nodes, if any",
|
||||||
|
)
|
||||||
|
|
||||||
|
def load(self, stream):
|
||||||
|
# type: (IO) -> etree
|
||||||
|
"""Load the stream as an svg xml etree and make a backup"""
|
||||||
|
document = load_svg(stream)
|
||||||
|
self.original_document = copy.deepcopy(document)
|
||||||
|
self.svg: SvgDocumentElement = document.getroot()
|
||||||
|
self.svg.selection.set(*self.options.ids)
|
||||||
|
if not self.svg.selection and self.select_all:
|
||||||
|
self.svg.selection = self.svg.descendants().filter(*self.select_all)
|
||||||
|
return document
|
||||||
|
|
||||||
|
|
||||||
|
class SvgOutputMixin(_Base): # pylint: disable=too-few-public-methods, abstract-method
|
||||||
|
"""
|
||||||
|
Expects the output document to be an svg document and will write an etree xml.
|
||||||
|
|
||||||
|
A template can be specified to kick off the svg document building process.
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = """<svg viewBox="0 0 {width} {height}"
|
||||||
|
width="{width}{unit}" height="{height}{unit}"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
|
||||||
|
</svg>"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_template(cls, **kwargs):
|
||||||
|
"""
|
||||||
|
Opens a template svg document for building, the kwargs
|
||||||
|
MUST include all the replacement values in the template, the
|
||||||
|
default template has 'width' and 'height' of the document.
|
||||||
|
"""
|
||||||
|
kwargs.setdefault("unit", "")
|
||||||
|
return load_svg(str(cls.template.format(**kwargs)))
|
||||||
|
|
||||||
|
def save(self, stream):
|
||||||
|
# type: (IO) -> None
|
||||||
|
"""Save the svg document to the given stream"""
|
||||||
|
if isinstance(self.document, (bytes, str)):
|
||||||
|
document = self.document
|
||||||
|
elif "Element" in type(self.document).__name__:
|
||||||
|
# isinstance can't be used here because etree is broken
|
||||||
|
doc = cast(etree, self.document)
|
||||||
|
document = doc.getroot().tostring()
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unknown type of document: {type(self.document).__name__} can not"
|
||||||
|
+ "save."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
stream.write(document)
|
||||||
|
except TypeError:
|
||||||
|
# we hope that this happens only when document needs to be encoded
|
||||||
|
stream.write(document.encode("utf-8")) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class SvgThroughMixin(SvgInputMixin, SvgOutputMixin): # pylint: disable=abstract-method
|
||||||
|
"""
|
||||||
|
Combine the input and output svg document handling (usually for effects).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def has_changed(self, ret): # pylint: disable=unused-argument
|
||||||
|
# type: (Any) -> bool
|
||||||
|
"""Return true if the svg document has changed"""
|
||||||
|
original = etree.tostring(self.original_document)
|
||||||
|
result = etree.tostring(self.document)
|
||||||
|
return original != result
|
||||||
582
deps/inkex/bezier.py
vendored
Normal file
582
deps/inkex/bezier.py
vendored
Normal file
@@ -0,0 +1,582 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2010 Nick Drobchenko, nick@cnc-club.ru
|
||||||
|
# Copyright (C) 2005 Aaron Spike, aaron@ekips.org
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# pylint: disable=invalid-name,too-many-locals
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Bezier calculations
|
||||||
|
"""
|
||||||
|
|
||||||
|
import cmath
|
||||||
|
import math
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
from .transforms import DirectedLineSegment
|
||||||
|
from .localization import inkex_gettext as _
|
||||||
|
|
||||||
|
# bez = ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))
|
||||||
|
|
||||||
|
|
||||||
|
def pointdistance(point_a, point_b):
|
||||||
|
"""The straight line distance between two points"""
|
||||||
|
return math.sqrt(
|
||||||
|
((point_b[0] - point_a[0]) ** 2) + ((point_b[1] - point_a[1]) ** 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def between_point(point_a, point_b, time=0.5):
|
||||||
|
"""Returns the point between point a and point b"""
|
||||||
|
return point_a[0] + time * (point_b[0] - point_a[0]), point_a[1] + time * (
|
||||||
|
point_b[1] - point_a[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def percent_point(point_a, point_b, percent=50.0):
|
||||||
|
"""Returns between_point but takes percent instead of 0.0-1.0"""
|
||||||
|
return between_point(point_a, point_b, percent / 100.0)
|
||||||
|
|
||||||
|
|
||||||
|
def root_wrapper(root_a, root_b, root_c, root_d):
|
||||||
|
"""Get the Cubic function, moic formular of roots, simple root"""
|
||||||
|
if root_a:
|
||||||
|
# Monics formula, see
|
||||||
|
# http://en.wikipedia.org/wiki/Cubic_function#Monic_formula_of_roots
|
||||||
|
mono_a, mono_b, mono_c = (root_b / root_a, root_c / root_a, root_d / root_a)
|
||||||
|
m = 2.0 * mono_a**3 - 9.0 * mono_a * mono_b + 27.0 * mono_c
|
||||||
|
k = mono_a**2 - 3.0 * mono_b
|
||||||
|
n = m**2 - 4.0 * k**3
|
||||||
|
w1 = -0.5 + 0.5 * cmath.sqrt(-3.0)
|
||||||
|
w2 = -0.5 - 0.5 * cmath.sqrt(-3.0)
|
||||||
|
if n < 0:
|
||||||
|
m1 = pow(complex((m + cmath.sqrt(n)) / 2), 1.0 / 3)
|
||||||
|
n1 = pow(complex((m - cmath.sqrt(n)) / 2), 1.0 / 3)
|
||||||
|
else:
|
||||||
|
if m + math.sqrt(n) < 0:
|
||||||
|
m1 = -pow(-(m + math.sqrt(n)) / 2, 1.0 / 3)
|
||||||
|
else:
|
||||||
|
m1 = pow((m + math.sqrt(n)) / 2, 1.0 / 3)
|
||||||
|
if m - math.sqrt(n) < 0:
|
||||||
|
n1 = -pow(-(m - math.sqrt(n)) / 2, 1.0 / 3)
|
||||||
|
else:
|
||||||
|
n1 = pow((m - math.sqrt(n)) / 2, 1.0 / 3)
|
||||||
|
return (
|
||||||
|
-1.0 / 3 * (mono_a + m1 + n1),
|
||||||
|
-1.0 / 3 * (mono_a + w1 * m1 + w2 * n1),
|
||||||
|
-1.0 / 3 * (mono_a + w2 * m1 + w1 * n1),
|
||||||
|
)
|
||||||
|
if root_b:
|
||||||
|
det = root_c**2.0 - 4.0 * root_b * root_d
|
||||||
|
if det:
|
||||||
|
return (
|
||||||
|
(-root_c + cmath.sqrt(det)) / (2.0 * root_b),
|
||||||
|
(-root_c - cmath.sqrt(det)) / (2.0 * root_b),
|
||||||
|
)
|
||||||
|
return (-root_c / (2.0 * root_b),)
|
||||||
|
if root_c:
|
||||||
|
return (1.0 * (-root_d / root_c),)
|
||||||
|
return ()
|
||||||
|
|
||||||
|
|
||||||
|
def bezlenapprx(sp1, sp2):
|
||||||
|
"""Return the aproximate length between two beziers"""
|
||||||
|
return (
|
||||||
|
pointdistance(sp1[1], sp1[2])
|
||||||
|
+ pointdistance(sp1[2], sp2[0])
|
||||||
|
+ pointdistance(sp2[0], sp2[1])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cspbezsplit(sp1, sp2, time=0.5):
|
||||||
|
"""Split a cubic bezier at the time period"""
|
||||||
|
m1 = tpoint(sp1[1], sp1[2], time)
|
||||||
|
m2 = tpoint(sp1[2], sp2[0], time)
|
||||||
|
m3 = tpoint(sp2[0], sp2[1], time)
|
||||||
|
m4 = tpoint(m1, m2, time)
|
||||||
|
m5 = tpoint(m2, m3, time)
|
||||||
|
m = tpoint(m4, m5, time)
|
||||||
|
return [[sp1[0][:], sp1[1][:], m1], [m4, m, m5], [m3, sp2[1][:], sp2[2][:]]]
|
||||||
|
|
||||||
|
|
||||||
|
def cspbezsplitatlength(sp1, sp2, length=0.5, tolerance=0.001):
|
||||||
|
"""Split a cubic bezier at length"""
|
||||||
|
bez = (sp1[1][:], sp1[2][:], sp2[0][:], sp2[1][:])
|
||||||
|
time = beziertatlength(bez, length, tolerance)
|
||||||
|
return cspbezsplit(sp1, sp2, time)
|
||||||
|
|
||||||
|
|
||||||
|
def cspseglength(sp1, sp2, tolerance=0.001):
|
||||||
|
"""Get cubic bezier segment length"""
|
||||||
|
bez = (sp1[1][:], sp1[2][:], sp2[0][:], sp2[1][:])
|
||||||
|
return bezierlength(bez, tolerance)
|
||||||
|
|
||||||
|
|
||||||
|
def csplength(csp):
|
||||||
|
"""Get cubic bezier length"""
|
||||||
|
total = 0
|
||||||
|
lengths = []
|
||||||
|
for sp in csp:
|
||||||
|
lengths.append([])
|
||||||
|
for i in range(1, len(sp)):
|
||||||
|
l = cspseglength(sp[i - 1], sp[i])
|
||||||
|
lengths[-1].append(l)
|
||||||
|
total += l
|
||||||
|
return lengths, total
|
||||||
|
|
||||||
|
|
||||||
|
def bezierparameterize(bez):
|
||||||
|
"""Return the bezier parameter size
|
||||||
|
Converts the bezier parametrisation from the default form
|
||||||
|
P(t) = (1-t)³ P_1 + 3(1-t)²t P_2 + 3(1-t)t² P_3 + t³ x_4
|
||||||
|
to the a form which can be differentiated more easily
|
||||||
|
P(t) = a t³ + b t² + c t + P0
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bez (List[Tuple[float, float]]): the Bezier curve. The elements of the list the
|
||||||
|
coordinates of the points (in this order): Start point, Start control point,
|
||||||
|
End control point, End point.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[float, float, float, float, float, float, float, float]:
|
||||||
|
the values ax, ay, bx, by, cx, cy, x0, y0
|
||||||
|
"""
|
||||||
|
((bx0, by0), (bx1, by1), (bx2, by2), (bx3, by3)) = bez
|
||||||
|
# parametric bezier
|
||||||
|
x0 = bx0
|
||||||
|
y0 = by0
|
||||||
|
cx = 3 * (bx1 - x0)
|
||||||
|
bx = 3 * (bx2 - bx1) - cx
|
||||||
|
ax = bx3 - x0 - cx - bx
|
||||||
|
cy = 3 * (by1 - y0)
|
||||||
|
by = 3 * (by2 - by1) - cy
|
||||||
|
ay = by3 - y0 - cy - by
|
||||||
|
|
||||||
|
return ax, ay, bx, by, cx, cy, x0, y0
|
||||||
|
|
||||||
|
|
||||||
|
def linebezierintersect(arg_a, bez):
|
||||||
|
"""Where a line and bezier intersect"""
|
||||||
|
((lx1, ly1), (lx2, ly2)) = arg_a
|
||||||
|
# parametric line
|
||||||
|
dd = lx1
|
||||||
|
cc = lx2 - lx1
|
||||||
|
bb = ly1
|
||||||
|
aa = ly2 - ly1
|
||||||
|
|
||||||
|
if aa:
|
||||||
|
coef1 = cc / aa
|
||||||
|
coef2 = 1
|
||||||
|
else:
|
||||||
|
coef1 = 1
|
||||||
|
coef2 = aa / cc
|
||||||
|
|
||||||
|
ax, ay, bx, by, cx, cy, x0, y0 = bezierparameterize(bez)
|
||||||
|
# cubic intersection coefficients
|
||||||
|
a = coef1 * ay - coef2 * ax
|
||||||
|
b = coef1 * by - coef2 * bx
|
||||||
|
c = coef1 * cy - coef2 * cx
|
||||||
|
d = coef1 * (y0 - bb) - coef2 * (x0 - dd)
|
||||||
|
|
||||||
|
roots = root_wrapper(a, b, c, d)
|
||||||
|
retval = []
|
||||||
|
for i in roots:
|
||||||
|
if isinstance(i, complex) and i.imag == 0:
|
||||||
|
i = i.real
|
||||||
|
if not isinstance(i, complex) and 0 <= i <= 1:
|
||||||
|
retval.append(bezierpointatt(bez, i))
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
def bezierpointatt(bez, t):
|
||||||
|
"""Get coords at the given time point along a bezier curve"""
|
||||||
|
ax, ay, bx, by, cx, cy, x0, y0 = bezierparameterize(bez)
|
||||||
|
x = ax * (t**3) + bx * (t**2) + cx * t + x0
|
||||||
|
y = ay * (t**3) + by * (t**2) + cy * t + y0
|
||||||
|
return x, y
|
||||||
|
|
||||||
|
|
||||||
|
def bezierslopeatt(bez, t):
|
||||||
|
"""Get slope at the given time point along a bezier curve
|
||||||
|
The slope is computed as (dx, dy) where dx = df_x(t)/dt and dy = df_y(t)/dt.
|
||||||
|
Note that for lines P1=P2 and P3=P4, so the slope at the end points is dx=dy=0
|
||||||
|
(slope not defined).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bez (List[Tuple[float, float]]): the Bezier curve. The elements of the list the
|
||||||
|
coordinates of the points (in this order): Start point, Start control point,
|
||||||
|
End control point, End point.
|
||||||
|
t (float): time in the interval [0, 1]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[float, float]: x and y increment
|
||||||
|
"""
|
||||||
|
ax, ay, bx, by, cx, cy, _, _ = bezierparameterize(bez)
|
||||||
|
dx = 3 * ax * (t**2) + 2 * bx * t + cx
|
||||||
|
dy = 3 * ay * (t**2) + 2 * by * t + cy
|
||||||
|
return dx, dy
|
||||||
|
|
||||||
|
|
||||||
|
def beziertatslope(bez, d):
|
||||||
|
"""Reverse; get time from slope along a bezier curve"""
|
||||||
|
ax, ay, bx, by, cx, cy, _, _ = bezierparameterize(bez)
|
||||||
|
(dy, dx) = d
|
||||||
|
# quadratic coefficients of slope formula
|
||||||
|
if dx:
|
||||||
|
slope = 1.0 * (dy / dx)
|
||||||
|
a = 3 * ay - 3 * ax * slope
|
||||||
|
b = 2 * by - 2 * bx * slope
|
||||||
|
c = cy - cx * slope
|
||||||
|
elif dy:
|
||||||
|
slope = 1.0 * (dx / dy)
|
||||||
|
a = 3 * ax - 3 * ay * slope
|
||||||
|
b = 2 * bx - 2 * by * slope
|
||||||
|
c = cx - cy * slope
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
roots = root_wrapper(0, a, b, c)
|
||||||
|
retval = []
|
||||||
|
for i in roots:
|
||||||
|
if isinstance(i, complex) and i.imag == 0:
|
||||||
|
i = i.real
|
||||||
|
if not isinstance(i, complex) and 0 <= i <= 1:
|
||||||
|
retval.append(i)
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
def tpoint(p1, p2, t):
|
||||||
|
"""Linearly interpolate between p1 and p2.
|
||||||
|
|
||||||
|
t = 0.0 returns p1, t = 1.0 returns p2.
|
||||||
|
|
||||||
|
:return: Interpolated point
|
||||||
|
:rtype: tuple
|
||||||
|
|
||||||
|
:param p1: First point as sequence of two floats
|
||||||
|
:param p2: Second point as sequence of two floats
|
||||||
|
:param t: Number between 0.0 and 1.0
|
||||||
|
:type t: float
|
||||||
|
"""
|
||||||
|
x1, y1 = p1
|
||||||
|
x2, y2 = p2
|
||||||
|
return x1 + t * (x2 - x1), y1 + t * (y2 - y1)
|
||||||
|
|
||||||
|
|
||||||
|
def beziersplitatt(bez, t):
|
||||||
|
"""Split bezier at given time"""
|
||||||
|
((bx0, by0), (bx1, by1), (bx2, by2), (bx3, by3)) = bez
|
||||||
|
m1 = tpoint((bx0, by0), (bx1, by1), t)
|
||||||
|
m2 = tpoint((bx1, by1), (bx2, by2), t)
|
||||||
|
m3 = tpoint((bx2, by2), (bx3, by3), t)
|
||||||
|
m4 = tpoint(m1, m2, t)
|
||||||
|
m5 = tpoint(m2, m3, t)
|
||||||
|
m = tpoint(m4, m5, t)
|
||||||
|
|
||||||
|
return ((bx0, by0), m1, m4, m), (m, m5, m3, (bx3, by3))
|
||||||
|
|
||||||
|
|
||||||
|
def addifclose(bez, l, error=0.001):
|
||||||
|
"""Gravesen, Add if the line is closed, in-place addition to array l"""
|
||||||
|
box = 0
|
||||||
|
for i in range(1, 4):
|
||||||
|
box += pointdistance(bez[i - 1], bez[i])
|
||||||
|
chord = pointdistance(bez[0], bez[3])
|
||||||
|
if (box - chord) > error:
|
||||||
|
first, second = beziersplitatt(bez, 0.5)
|
||||||
|
addifclose(first, l, error)
|
||||||
|
addifclose(second, l, error)
|
||||||
|
else:
|
||||||
|
l[0] += (box / 2.0) + (chord / 2.0)
|
||||||
|
|
||||||
|
|
||||||
|
# balfax, balfbx, balfcx, balfay, balfby, balfcy = 0, 0, 0, 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
def balf(t, args):
|
||||||
|
"""Bezier Arc Length Function"""
|
||||||
|
ax, bx, cx, ay, by, cy = args
|
||||||
|
retval = (ax * (t**2) + bx * t + cx) ** 2 + (ay * (t**2) + by * t + cy) ** 2
|
||||||
|
return math.sqrt(retval)
|
||||||
|
|
||||||
|
|
||||||
|
def simpson(start, end, maxiter, tolerance, bezier_args):
|
||||||
|
"""Calculate the length of a bezier curve using Simpson's algorithm:
|
||||||
|
http://steve.hollasch.net/cgindex/curves/cbezarclen.html
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start (int): Start time (between 0 and 1)
|
||||||
|
end (int): End time (between start time and 1)
|
||||||
|
maxiter (int): Maximum number of iterations. If not a power of 2, the algorithm
|
||||||
|
will behave like the value is set to the next power of 2.
|
||||||
|
tolerance (float): maximum error ratio
|
||||||
|
bezier_args (list): arguments as computed by bezierparametrize()
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: the appoximate length of the bezier curve
|
||||||
|
"""
|
||||||
|
|
||||||
|
n = 2
|
||||||
|
multiplier = (end - start) / 6.0
|
||||||
|
endsum = balf(start, bezier_args) + balf(end, bezier_args)
|
||||||
|
interval = (end - start) / 2.0
|
||||||
|
asum = 0.0
|
||||||
|
bsum = balf(start + interval, bezier_args)
|
||||||
|
est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum))
|
||||||
|
est0 = 2.0 * est1
|
||||||
|
# print(multiplier, endsum, interval, asum, bsum, est1, est0)
|
||||||
|
while n < maxiter and abs(est1 - est0) > tolerance:
|
||||||
|
n *= 2
|
||||||
|
multiplier /= 2.0
|
||||||
|
interval /= 2.0
|
||||||
|
asum += bsum
|
||||||
|
bsum = 0.0
|
||||||
|
est0 = est1
|
||||||
|
for i in range(1, n, 2):
|
||||||
|
bsum += balf(start + (i * interval), bezier_args)
|
||||||
|
est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum))
|
||||||
|
# print(multiplier, endsum, interval, asum, bsum, est1, est0)
|
||||||
|
return est1
|
||||||
|
|
||||||
|
|
||||||
|
def bezierlength(bez, tolerance=0.001, time=1.0):
|
||||||
|
"""Get length of bezier curve"""
|
||||||
|
ax, ay, bx, by, cx, cy, _, _ = bezierparameterize(bez)
|
||||||
|
return simpson(0.0, time, 4096, tolerance, [3 * ax, 2 * bx, cx, 3 * ay, 2 * by, cy])
|
||||||
|
|
||||||
|
|
||||||
|
def beziertatlength(bez, l=0.5, tolerance=0.001):
|
||||||
|
"""Get bezier curve time at the length specified"""
|
||||||
|
curlen = bezierlength(bez, tolerance, 1.0)
|
||||||
|
time = 1.0
|
||||||
|
tdiv = time
|
||||||
|
targetlen = l * curlen
|
||||||
|
diff = curlen - targetlen
|
||||||
|
while abs(diff) > tolerance:
|
||||||
|
tdiv /= 2.0
|
||||||
|
if diff < 0:
|
||||||
|
time += tdiv
|
||||||
|
else:
|
||||||
|
time -= tdiv
|
||||||
|
curlen = bezierlength(bez, tolerance, time)
|
||||||
|
diff = curlen - targetlen
|
||||||
|
return time
|
||||||
|
|
||||||
|
|
||||||
|
def maxdist(bez):
|
||||||
|
"""Get maximum distance within bezier curve"""
|
||||||
|
seg = DirectedLineSegment(bez[0], bez[3])
|
||||||
|
return max(seg.distance_to_point(*bez[1]), seg.distance_to_point(*bez[2]))
|
||||||
|
|
||||||
|
|
||||||
|
def cspsubdiv(csp, flat):
|
||||||
|
"""Sub-divide cubic sub-paths"""
|
||||||
|
for sp in csp:
|
||||||
|
subdiv(sp, flat)
|
||||||
|
|
||||||
|
|
||||||
|
def subdiv(sp, flat, i=1):
|
||||||
|
"""sub divide bezier curve"""
|
||||||
|
while i < len(sp):
|
||||||
|
p0 = sp[i - 1][1]
|
||||||
|
p1 = sp[i - 1][2]
|
||||||
|
p2 = sp[i][0]
|
||||||
|
p3 = sp[i][1]
|
||||||
|
|
||||||
|
bez = (p0, p1, p2, p3)
|
||||||
|
mdist = maxdist(bez)
|
||||||
|
if mdist <= flat:
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
one, two = beziersplitatt(bez, 0.5)
|
||||||
|
sp[i - 1][2] = one[1]
|
||||||
|
sp[i][0] = two[2]
|
||||||
|
p = [one[2], one[3], two[1]]
|
||||||
|
sp[i:1] = [p]
|
||||||
|
|
||||||
|
|
||||||
|
def csparea(csp):
|
||||||
|
r"""Get total area of cubic superpath.
|
||||||
|
|
||||||
|
.. hint::
|
||||||
|
|
||||||
|
The results may be slightly inaccurate for paths containing arcs due
|
||||||
|
to the loss of accuracy during arc -> cubic bezier conversion.
|
||||||
|
|
||||||
|
|
||||||
|
The function works as follows: For each subpath,
|
||||||
|
|
||||||
|
#. compute the area of the polygon created by the path's vertices:
|
||||||
|
|
||||||
|
For a line with coordinates :math:`(x_0, y_0)` and :math:`(x_1, y_1)`, the area
|
||||||
|
of the trapezoid of its projection on the x axis is given by
|
||||||
|
|
||||||
|
.. math::
|
||||||
|
|
||||||
|
\frac{1}{2} (y_1 + y_0) (x_1 - x_0)
|
||||||
|
|
||||||
|
Summing the contribution of all lines of the polygon yields the polygon's area
|
||||||
|
(lines from left to right have a positive contribution, while those right-to
|
||||||
|
left have a negative area contribution, canceling out the computed area not
|
||||||
|
inside the polygon), so we find (setting :math:`x_{0} = x_N` etc.):
|
||||||
|
|
||||||
|
.. math::
|
||||||
|
|
||||||
|
A = \frac{1}{2} * \sum_{i=1}^N (x_i y_i - x_{i-1} y_{i-1} + x_i y_{i-1}
|
||||||
|
- x_{i-1} y_{i})
|
||||||
|
|
||||||
|
The first two terms cancel out in the summation over all points, and the second
|
||||||
|
two terms can be regrouped as
|
||||||
|
|
||||||
|
.. math::
|
||||||
|
|
||||||
|
A = \frac{1}{2} * \sum_{i=1}^N x_i (y_{i+1} -y_{i-1})
|
||||||
|
|
||||||
|
#. The contribution by the bezier curve is considered: We compute
|
||||||
|
the integral :math:`\int_{x(t=0)}^{x(t=1)} y dx`, i.e. the area between the x
|
||||||
|
axis and the curve, where :math:`y = y(t)` (the Bezier curve). By substitution
|
||||||
|
:math:`dx = x'(t) dt`, performing the integration and
|
||||||
|
subtracting the trapezoid we already considered above, we find (with control
|
||||||
|
points :math:`(x_{c1}, y_{c1})` and :math:`(x_{c2}, y_{c2})`)
|
||||||
|
|
||||||
|
.. math::
|
||||||
|
|
||||||
|
\Delta A &= \int_0^1 y(t) x'(t) dt - \frac{1}{2} (y_1 + y_0) (x_1 - x_0) \\
|
||||||
|
&= \frac{3}{20} \cdot \begin{pmatrix}
|
||||||
|
& y_0(& & 2x_{c1} & + x_{c2} & -3x_1&) \\
|
||||||
|
+ & y_{c1}(& -2x_0 & & + x_{c2} &+ x_1&) \\
|
||||||
|
+ & y_{c2}(& -x_0 & -x_{c1} & & + 2x_1&) \\
|
||||||
|
+ & y_1(& 3x_0 & - x_{c1} & -2 x_{c2} &&)
|
||||||
|
\end{pmatrix}
|
||||||
|
|
||||||
|
This is computed for every bezier and added to the area. Again, this is a signed
|
||||||
|
area: convex beziers have a positive area and concave ones a negative area
|
||||||
|
contribution.
|
||||||
|
"""
|
||||||
|
|
||||||
|
MAT_AREA = numpy.array(
|
||||||
|
[[0, 2, 1, -3], [-2, 0, 1, 1], [-1, -1, 0, 2], [3, -1, -2, 0]]
|
||||||
|
)
|
||||||
|
area = 0.0
|
||||||
|
for sp in csp:
|
||||||
|
if len(sp) < 2:
|
||||||
|
continue
|
||||||
|
for x, coord in enumerate(sp): # calculate polygon area
|
||||||
|
area += 0.5 * sp[x - 1][1][0] * (coord[1][1] - sp[x - 2][1][1])
|
||||||
|
for i in range(1, len(sp)): # add contribution from cubic Bezier
|
||||||
|
# EXPLANATION: https://github.com/Pomax/BezierInfo-2/issues/238#issue-554619801
|
||||||
|
vec_x = numpy.array(
|
||||||
|
[sp[i - 1][1][0], sp[i - 1][2][0], sp[i][0][0], sp[i][1][0]]
|
||||||
|
)
|
||||||
|
vec_y = numpy.array(
|
||||||
|
[sp[i - 1][1][1], sp[i - 1][2][1], sp[i][0][1], sp[i][1][1]]
|
||||||
|
)
|
||||||
|
vex = numpy.matmul(vec_x, MAT_AREA)
|
||||||
|
area += 0.15 * numpy.matmul(vex, vec_y.T)
|
||||||
|
return -area
|
||||||
|
|
||||||
|
|
||||||
|
def cspcofm(csp):
|
||||||
|
r"""Get center of area / gravity for a cubic superpath.
|
||||||
|
|
||||||
|
.. hint::
|
||||||
|
|
||||||
|
The results may be slightly inaccurate for paths containing arcs due
|
||||||
|
to the loss of accuracy during arc -> cubic bezier conversion.
|
||||||
|
|
||||||
|
The function works similar to :func:`csparea`, only the computations are a bit more
|
||||||
|
difficult. Again all subpaths are considered. The total center of mass is given by
|
||||||
|
|
||||||
|
.. math::
|
||||||
|
|
||||||
|
C_y = \frac{1}{A} \int_A y dA
|
||||||
|
|
||||||
|
The integral can be expressed as a weighted sum; first, the contributions
|
||||||
|
of the polygon created by the path's nodes is computed. Second, we compute the
|
||||||
|
contribution of the Bezier curve; this is again done by an integral from which
|
||||||
|
the weighted CofM of the trapezoid between end points and horizontal axis is
|
||||||
|
removed. For the integrals, we have
|
||||||
|
|
||||||
|
.. math::
|
||||||
|
|
||||||
|
A * C_{y,bez} &= \int_A y dA = \int_{x(t=0)}^{y(t=1)} \int_{0}^{y(x)} y dy dx \\
|
||||||
|
&= \int_{x(t=0)}^{y(t=1)} \frac 12 y(x)^2 dx
|
||||||
|
= \int_0^1 \frac 12 y(t)^2 x'(t) dt \\
|
||||||
|
A * C_{x,bez} &= \int_A x dA = \int_{x(t=0)}^{y(t=1)} x \int_{0}^{y(x)} dy dx \\
|
||||||
|
&= \int_{x(t=0)}^{y(t=1)} x y(x) dx = \int_0^1 x(t) y(t) x'(t) dt
|
||||||
|
|
||||||
|
from which the trapezoids are removed, in case of the y-CofM this amounts to
|
||||||
|
|
||||||
|
.. math::
|
||||||
|
|
||||||
|
\frac{y_0}{2} (x_1-x_0)y_0 + \left(y_0 + \frac 13 (y_1 - y_0)\right)
|
||||||
|
\cdot \frac 12 (y_1 - y_0) (x_1 - x_0)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
MAT_COFM_0 = numpy.array(
|
||||||
|
[[0, 35, 10, -45], [-35, 0, 12, 23], [-10, -12, 0, 22], [45, -23, -22, 0]]
|
||||||
|
)
|
||||||
|
|
||||||
|
MAT_COFM_1 = numpy.array(
|
||||||
|
[[0, 15, 3, -18], [-15, 0, 9, 6], [-3, -9, 0, 12], [18, -6, -12, 0]]
|
||||||
|
)
|
||||||
|
|
||||||
|
MAT_COFM_2 = numpy.array(
|
||||||
|
[[0, 12, 6, -18], [-12, 0, 9, 3], [-6, -9, 0, 15], [18, -3, -15, 0]]
|
||||||
|
)
|
||||||
|
|
||||||
|
MAT_COFM_3 = numpy.array(
|
||||||
|
[[0, 22, 23, -45], [-22, 0, 12, 10], [-23, -12, 0, 35], [45, -10, -35, 0]]
|
||||||
|
)
|
||||||
|
area = csparea(csp)
|
||||||
|
xc = 0.0
|
||||||
|
yc = 0.0
|
||||||
|
if abs(area) < 1.0e-8:
|
||||||
|
raise ValueError(_("Area is zero, cannot calculate Center of Mass"))
|
||||||
|
for sp in csp:
|
||||||
|
for x, coord in enumerate(sp): # calculate polygon moment
|
||||||
|
xc += (
|
||||||
|
sp[x - 1][1][1]
|
||||||
|
* (sp[x - 2][1][0] - coord[1][0])
|
||||||
|
* (sp[x - 2][1][0] + sp[x - 1][1][0] + coord[1][0])
|
||||||
|
/ 6
|
||||||
|
)
|
||||||
|
yc += (
|
||||||
|
sp[x - 1][1][0]
|
||||||
|
* (coord[1][1] - sp[x - 2][1][1])
|
||||||
|
* (sp[x - 2][1][1] + sp[x - 1][1][1] + coord[1][1])
|
||||||
|
/ 6
|
||||||
|
)
|
||||||
|
for i in range(1, len(sp)): # add contribution from cubic Bezier
|
||||||
|
vec_x = numpy.array(
|
||||||
|
[sp[i - 1][1][0], sp[i - 1][2][0], sp[i][0][0], sp[i][1][0]]
|
||||||
|
)
|
||||||
|
vec_y = numpy.array(
|
||||||
|
[sp[i - 1][1][1], sp[i - 1][2][1], sp[i][0][1], sp[i][1][1]]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _mul(MAT, vec_x=vec_x, vec_y=vec_y):
|
||||||
|
return numpy.matmul(numpy.matmul(vec_x, MAT), vec_y.T)
|
||||||
|
|
||||||
|
vec_t = numpy.array(
|
||||||
|
[_mul(MAT_COFM_0), _mul(MAT_COFM_1), _mul(MAT_COFM_2), _mul(MAT_COFM_3)]
|
||||||
|
)
|
||||||
|
xc += numpy.matmul(vec_x, vec_t.T) / 280
|
||||||
|
yc += numpy.matmul(vec_y, vec_t.T) / 280
|
||||||
|
return -xc / area, -yc / area
|
||||||
49
deps/inkex/colors/__init__.py
vendored
Normal file
49
deps/inkex/colors/__init__.py
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
"""
|
||||||
|
The color module allows for the parsing and printing of CSS colors in an SVG document.
|
||||||
|
|
||||||
|
Support formats are currently:
|
||||||
|
|
||||||
|
1. #RGB #RRGGBB #RGBA #RRGGBBAA formats
|
||||||
|
2. Named colors such as 'red'
|
||||||
|
3. icc-color(...) which is specific to SVG 1.1
|
||||||
|
4. rgb(...) and rgba(...) from CSS Color Module 3
|
||||||
|
5. hsl(...) and hsla(...) from CSS Color Module 3
|
||||||
|
6. hwb(...) from CSS Color Module 4, but encoded internally as hsv
|
||||||
|
7. device-cmyk(...) from CSS Color Module 4
|
||||||
|
|
||||||
|
Each color space has it's own class, such as ColorRGB. Each space will parse multiple
|
||||||
|
formats, for example ColorRGB supports hex and rgb CSS module formats.
|
||||||
|
|
||||||
|
Each color object is a list of numbers, each number is a channel in that color space
|
||||||
|
with alpha channel being held in it's own property which may be a unit number or None.
|
||||||
|
|
||||||
|
The numbers a color stores are typically in the range defined in the CSS module
|
||||||
|
specification so for example RGB, all the numbers are between 0-255 while for hsl
|
||||||
|
the hue channel is between 0-360 and the saturation and lightness are between 0-100.
|
||||||
|
|
||||||
|
To get normalised numbers you can use to the `to_units` function to get everything 0-1
|
||||||
|
|
||||||
|
Each Color space type has a name value which can be used to identify the color space,
|
||||||
|
if this is more useful than checking the class type. Either can be used when converting
|
||||||
|
the color values between spaces.
|
||||||
|
|
||||||
|
A color object may be converted into a different space by using the
|
||||||
|
`color.to(other_space)` function, which will return a new color object in the requested
|
||||||
|
space.
|
||||||
|
|
||||||
|
There are three special cases.
|
||||||
|
|
||||||
|
1. ColorNamed is a type of ColorRGB which will preferentially print the name instead
|
||||||
|
of the hex value if one is available.
|
||||||
|
2. ColorNone is a special value which indicates the keyword `none` and does not
|
||||||
|
allow any values or alpha.
|
||||||
|
3. ColorCMS can not be converted to other color spaces and contains a `fallback_color`
|
||||||
|
to access the RGB fallback if it was provided.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .color import Color, ColorError, ColorIdError
|
||||||
|
from .utils import is_color
|
||||||
|
|
||||||
|
from .spaces import *
|
||||||
BIN
deps/inkex/colors/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/__pycache__/color.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/__pycache__/color.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/__pycache__/color.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/__pycache__/color.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/__pycache__/converters.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/__pycache__/converters.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/__pycache__/converters.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/__pycache__/converters.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/__pycache__/utils.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/__pycache__/utils.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/__pycache__/utils.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/__pycache__/utils.cpython-313.pyc
vendored
Normal file
Binary file not shown.
295
deps/inkex/colors/color.py
vendored
Normal file
295
deps/inkex/colors/color.py
vendored
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2020 Martin Owens
|
||||||
|
# 2021 Jonathan Neuhauser
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Basic color controls
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from .converters import Converters
|
||||||
|
|
||||||
|
Number = Union[int, float]
|
||||||
|
|
||||||
|
|
||||||
|
def round_by_type(kind, number):
|
||||||
|
"""Round a number to zero or five decimal places depending on it's type"""
|
||||||
|
return kind(round(number, kind == float and 5 or 0))
|
||||||
|
|
||||||
|
|
||||||
|
class ColorError(KeyError):
|
||||||
|
"""Specific color parsing error"""
|
||||||
|
|
||||||
|
|
||||||
|
class ColorIdError(ColorError):
|
||||||
|
"""Special color error for gradient and color stop ids"""
|
||||||
|
|
||||||
|
|
||||||
|
class Color(list):
|
||||||
|
"""A parsed color object which could be in many color spaces, the default is sRGB
|
||||||
|
|
||||||
|
Can be constructed from valid CSS color attributes, as well as
|
||||||
|
tuple/list + color space. Percentage values are supported.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_spaces: Dict[str, type] = {}
|
||||||
|
|
||||||
|
name: Optional[str] = None
|
||||||
|
|
||||||
|
# A list of known channels
|
||||||
|
channels: Tuple[str, ...] = ()
|
||||||
|
|
||||||
|
# A list of scales for converting css color values to known qantities
|
||||||
|
scales: Tuple[
|
||||||
|
Union[Tuple[Number, Number, bool], Tuple[Number, Number]], ...
|
||||||
|
] = () # Min (int/float), Max (int/float), [wrap around (bool:False)]
|
||||||
|
|
||||||
|
# If alpha is not specified, this is the default for most color types.
|
||||||
|
default_alpha = 1.0
|
||||||
|
|
||||||
|
def __init_subclass__(cls):
|
||||||
|
if not cls.name:
|
||||||
|
return # It is a base class
|
||||||
|
|
||||||
|
# Add space to a dictionary of available color spaces
|
||||||
|
cls._spaces[cls.name] = cls
|
||||||
|
|
||||||
|
Converters.add_space(cls)
|
||||||
|
|
||||||
|
def __new__(cls, value=None, alpha=None, arg=None):
|
||||||
|
if not cls.name:
|
||||||
|
if value is None:
|
||||||
|
return super().__new__(cls._spaces["none"])
|
||||||
|
|
||||||
|
if isinstance(value, int):
|
||||||
|
return super().__new__(cls._spaces["rgb"])
|
||||||
|
|
||||||
|
if isinstance(value, str):
|
||||||
|
# String from xml or css attributes
|
||||||
|
for space in cls._spaces.values():
|
||||||
|
if space.can_parse(value.lower()):
|
||||||
|
return super().__new__(space, value)
|
||||||
|
|
||||||
|
if isinstance(value, Color):
|
||||||
|
return super().__new__(type(value), value)
|
||||||
|
|
||||||
|
if isinstance(value, (list, tuple)):
|
||||||
|
from ..deprecated.main import _deprecated
|
||||||
|
|
||||||
|
_deprecated(
|
||||||
|
"Anonymous lists of numbers for colors no longer default to rgb"
|
||||||
|
)
|
||||||
|
return super().__new__(cls._spaces["rgb"], value)
|
||||||
|
|
||||||
|
return super().__new__(cls, value, alpha=alpha, arg=arg)
|
||||||
|
|
||||||
|
def __init__(self, values, alpha=None, arg=None):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
if not self.name:
|
||||||
|
raise ColorError(f"Not a known color value: '{values}' {arg}")
|
||||||
|
|
||||||
|
if not isinstance(values, (list, tuple)):
|
||||||
|
raise ColorError(
|
||||||
|
f"Colors must be constructed with a list of values: '{values}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
if alpha is not None and not isinstance(alpha, float):
|
||||||
|
raise ColorError("Color alpha property must be a float number")
|
||||||
|
|
||||||
|
if alpha is None and self.channels and len(values) == len(self.channels) + 1:
|
||||||
|
alpha = values.pop()
|
||||||
|
|
||||||
|
if isinstance(values, Color):
|
||||||
|
alpha = values.alpha
|
||||||
|
|
||||||
|
if self.channels and len(values) != len(self.channels):
|
||||||
|
raise ColorError(
|
||||||
|
f"You must have {len(self.channels)} channels for a {self.name} color"
|
||||||
|
)
|
||||||
|
|
||||||
|
self[:] = values
|
||||||
|
self.alpha = alpha
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
"""Allow colors to be hashable"""
|
||||||
|
return tuple(self + [self.alpha, self.name]).__hash__()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"Color space {self.name} can not be printed to a string."
|
||||||
|
)
|
||||||
|
|
||||||
|
def __int__(self):
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"Color space {self.name} can not be converted to a number."
|
||||||
|
)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
"""Get the color value"""
|
||||||
|
space = self.name
|
||||||
|
|
||||||
|
if (
|
||||||
|
isinstance(index, slice)
|
||||||
|
and index.start is not None
|
||||||
|
and not isinstance(index.start, int)
|
||||||
|
):
|
||||||
|
# We support the format `value = color["space_name":index]` here
|
||||||
|
space = self._spaces[index.start]
|
||||||
|
index = int(index.stop)
|
||||||
|
|
||||||
|
# Allow regular slicing to fall through more freely than setitem
|
||||||
|
if space == self.name:
|
||||||
|
return super().__getitem__(index)
|
||||||
|
|
||||||
|
if not isinstance(index, int):
|
||||||
|
raise ColorError(f"Unknown color getter definition: '{index}'")
|
||||||
|
|
||||||
|
return self.to(space)[
|
||||||
|
index
|
||||||
|
] # Note: this calls Color.__getitem__ function again
|
||||||
|
|
||||||
|
def __setitem__(self, index, value):
|
||||||
|
"""Set the color value in place, limits setter to specific color space"""
|
||||||
|
space = self.name
|
||||||
|
|
||||||
|
if isinstance(index, slice):
|
||||||
|
# Support the format color[:] = [list of numbers] here
|
||||||
|
if index.start is None and index.stop is None:
|
||||||
|
super().__setitem__(
|
||||||
|
index, (self.constrain(ind, val) for ind, val in enumerate(value))
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# We support the format `color["space_name":index] = value` here
|
||||||
|
space = self._spaces[index.start]
|
||||||
|
index = int(index.stop)
|
||||||
|
|
||||||
|
if not isinstance(index, int):
|
||||||
|
raise ColorError(f"Unknown color setter definition: '{index}'")
|
||||||
|
|
||||||
|
# Setting a channel in the existing space
|
||||||
|
if space == self.name:
|
||||||
|
super().__setitem__(index, self.constrain(index, value))
|
||||||
|
else:
|
||||||
|
# Set channel is another space, convert back and forth
|
||||||
|
values = self.to(space)
|
||||||
|
values[index] = value # Note: this calls Color.__setitem__ function again
|
||||||
|
self[:] = values.to(self.name)
|
||||||
|
|
||||||
|
def to(self, space): # pylint: disable=invalid-name
|
||||||
|
"""Get this color but in a specific color space"""
|
||||||
|
if space in self._spaces.values():
|
||||||
|
space = space.name
|
||||||
|
if space not in self._spaces:
|
||||||
|
raise AttributeError(
|
||||||
|
f"Unknown color space {space} when converting from {self.name}"
|
||||||
|
)
|
||||||
|
if not hasattr(type(self), f"to_{space}"):
|
||||||
|
setattr(
|
||||||
|
type(self),
|
||||||
|
f"to_{space}",
|
||||||
|
Converters.find_converter(type(self), self._spaces[space]),
|
||||||
|
)
|
||||||
|
return getattr(self, f"to_{space}")()
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if name.startswith("to_") and name.count("_") == 1:
|
||||||
|
return lambda: self.to(name.split("_")[-1])
|
||||||
|
raise AttributeError(f"Can not find attribute {type(self).__name__}.{name}")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effective_alpha(self):
|
||||||
|
"""Get the alpha as set, or tell me what it would be by default"""
|
||||||
|
if self.alpha is None:
|
||||||
|
return self.default_alpha
|
||||||
|
return self.alpha
|
||||||
|
|
||||||
|
def get_values(self, alpha=True):
|
||||||
|
"""Returns all values, including alpha as a list"""
|
||||||
|
if alpha:
|
||||||
|
return list(self + [self.effective_alpha])
|
||||||
|
return list(self)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def to_units(cls, *values):
|
||||||
|
"""Convert the color values into floats scales from 0.0 to 1.0"""
|
||||||
|
return [cls.scale_down(ind, val) for ind, val in enumerate(values)]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_units(cls, *values):
|
||||||
|
"""Convert float values to the scales expected and return a new instance"""
|
||||||
|
return [cls.scale_up(ind, val) for ind, val in enumerate(values)]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def can_parse(cls, string): # pylint: disable=unused-argument
|
||||||
|
"""Returns true if this string can be parsed for this color type"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def scale_up(cls, index, value):
|
||||||
|
"""Convert from float 0.0 to 1.0 to an int used in css"""
|
||||||
|
(min_value, max_value) = cls.scales[index][:2]
|
||||||
|
return cls.constrain(
|
||||||
|
index, (value * (max_value - min_value)) + min_value
|
||||||
|
) # See inkscape/src/colors/spaces/base.h:SCALE_UP
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def scale_down(cls, index, value):
|
||||||
|
"""Convert from int, often 0 to 255 to a float 0.0 to 1.0"""
|
||||||
|
(min_value, max_value) = cls.scales[index][:2]
|
||||||
|
return (cls.constrain(index, value) - min_value) / (
|
||||||
|
max_value - min_value
|
||||||
|
) # See inkscape/src/colors/spaces/base.h:SCALE_DOWN
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def constrain(cls, index, value):
|
||||||
|
"""Constrains the value to the css scale"""
|
||||||
|
scale = cls.scales[index]
|
||||||
|
if len(scale) == 3 and scale[2] is True:
|
||||||
|
if value == scale[1]:
|
||||||
|
return value
|
||||||
|
return round_by_type(
|
||||||
|
type(scale[0]), value % scale[1]
|
||||||
|
) # Wrap around value (i.e. hue)
|
||||||
|
return min(max(round_by_type(type(scale[0]), value), scale[0]), scale[1])
|
||||||
|
|
||||||
|
def interpolate(self, other, fraction):
|
||||||
|
"""Interpolate two colours by the given fraction
|
||||||
|
|
||||||
|
.. versionadded:: 1.1"""
|
||||||
|
from ..tween import ColorInterpolator # pylint: disable=import-outside-toplevel
|
||||||
|
|
||||||
|
try:
|
||||||
|
other = other.to(type(self))
|
||||||
|
except ColorError:
|
||||||
|
raise ColorError("Can not convert color in interpolation.")
|
||||||
|
return ColorInterpolator(self, other).interpolate(fraction)
|
||||||
|
|
||||||
|
|
||||||
|
class AlphaNotAllowed:
|
||||||
|
"""Mixin class to indicate that alpha values are not permitted on this color space"""
|
||||||
|
|
||||||
|
alpha = property(
|
||||||
|
lambda self: None,
|
||||||
|
lambda self, value: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_values(self, alpha=False):
|
||||||
|
return super().get_values(False)
|
||||||
122
deps/inkex/colors/converters.py
vendored
Normal file
122
deps/inkex/colors/converters.py
vendored
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2018-2024 Martin Owens
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Basic color errors and common functions
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from typing import Dict, List, Callable
|
||||||
|
|
||||||
|
ConverterFunc = Callable[[float], List[float]]
|
||||||
|
|
||||||
|
|
||||||
|
class Converters:
|
||||||
|
"""
|
||||||
|
Record how colors can be converted between different spaces and provides
|
||||||
|
a way to path-find between multiple step conversions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
links: Dict[str, Dict[str, ConverterFunc]] = defaultdict(dict)
|
||||||
|
chains: Dict[str, List[List[str]]] = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_space(cls, color_cls):
|
||||||
|
"""
|
||||||
|
Records the stated links between this class and other color spaces
|
||||||
|
"""
|
||||||
|
for name, func in color_cls.__dict__.items():
|
||||||
|
if not name.startswith("convert_"):
|
||||||
|
continue
|
||||||
|
_, direction, space = name.split("_", 2)
|
||||||
|
from_name = color_cls.name if direction == "to" else space
|
||||||
|
to_name = color_cls.name if direction == "from" else space
|
||||||
|
|
||||||
|
if from_name != to_name:
|
||||||
|
if not isinstance(func, staticmethod):
|
||||||
|
raise TypeError(f"Method '{name}' must be a static method.")
|
||||||
|
cls.links[from_name][to_name] = func.__func__
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_chain(cls, source, target):
|
||||||
|
"""
|
||||||
|
Get a chain of conversions between two color spaces, if possible.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def build_chains(chains, space):
|
||||||
|
new_chains = []
|
||||||
|
for chain in chains:
|
||||||
|
for hop in cls.links[space]:
|
||||||
|
if hop not in chain:
|
||||||
|
new_chains += build_chains([chain + [hop]], hop)
|
||||||
|
return chains + new_chains
|
||||||
|
|
||||||
|
if source not in cls.chains:
|
||||||
|
cls.chains[source] = build_chains([[source]], source)
|
||||||
|
|
||||||
|
chosen = None
|
||||||
|
for chain in cls.chains[source] or ():
|
||||||
|
if chain[-1] == target and (not chosen or len(chain) < len(chosen)):
|
||||||
|
chosen = chain
|
||||||
|
return chosen
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find_converter(cls, source, target):
|
||||||
|
"""
|
||||||
|
Find a way to convert from source to target using any conversion functions.
|
||||||
|
|
||||||
|
Will hop from one space to another if needed.
|
||||||
|
"""
|
||||||
|
func = None
|
||||||
|
|
||||||
|
# Passthough
|
||||||
|
if source == target:
|
||||||
|
return lambda self: self
|
||||||
|
|
||||||
|
if func is None:
|
||||||
|
chain = cls.get_chain(source.name, target.name)
|
||||||
|
if chain:
|
||||||
|
return cls.generate_converter(chain, source, target)
|
||||||
|
|
||||||
|
# Returning a function means we only run this function once, even when not found
|
||||||
|
def _error(self):
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"Color space {source} can not be converted to {target}."
|
||||||
|
)
|
||||||
|
|
||||||
|
return _error
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_converter(cls, chain, source_cls, target_cls):
|
||||||
|
"""
|
||||||
|
Put together a function that can do every step of the chain of conversions
|
||||||
|
"""
|
||||||
|
# Build a list of functions to run
|
||||||
|
funcs = [cls.links[a][b] for a, b in zip(chain, chain[1:])]
|
||||||
|
funcs.insert(0, source_cls.to_units)
|
||||||
|
funcs.append(target_cls.from_units)
|
||||||
|
|
||||||
|
def _inner(values):
|
||||||
|
if hasattr(values, "alpha") and values.alpha is not None:
|
||||||
|
values = list(values) + [values.alpha]
|
||||||
|
for func in funcs:
|
||||||
|
values = func(*values)
|
||||||
|
return target_cls(values)
|
||||||
|
|
||||||
|
return _inner
|
||||||
11
deps/inkex/colors/spaces/__init__.py
vendored
Normal file
11
deps/inkex/colors/spaces/__init__.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"""
|
||||||
|
Each color space that this module supports such have one file in this module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .cmyk import ColorDeviceCMYK
|
||||||
|
from .cms import ColorCMS
|
||||||
|
from .hsl import ColorHSL
|
||||||
|
from .hsv import ColorHSV
|
||||||
|
from .named import ColorNamed
|
||||||
|
from .none import ColorNone
|
||||||
|
from .rgb import ColorRGB
|
||||||
BIN
deps/inkex/colors/spaces/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/cms.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/cms.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/cms.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/cms.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/cmyk.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/cmyk.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/cmyk.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/cmyk.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/css.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/css.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/css.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/css.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/hsl.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/hsl.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/hsl.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/hsl.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/hsv.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/hsv.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/hsv.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/hsv.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/named.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/named.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/named.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/named.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/none.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/none.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/none.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/none.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/rgb.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/rgb.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/rgb.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/rgb.cpython-313.pyc
vendored
Normal file
Binary file not shown.
95
deps/inkex/colors/spaces/cms.py
vendored
Normal file
95
deps/inkex/colors/spaces/cms.py
vendored
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 Martin Owens
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
SVG icc-color parser
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ..color import Color, AlphaNotAllowed, ColorError, round_by_type
|
||||||
|
from .css import CssColor
|
||||||
|
from .rgb import ColorRGB
|
||||||
|
|
||||||
|
|
||||||
|
class ColorCMS(CssColor, AlphaNotAllowed):
|
||||||
|
"""
|
||||||
|
Parse and print SVG icc-color objects into their values and the fallback RGB
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "cms"
|
||||||
|
css_func = "icc-color"
|
||||||
|
channels = ()
|
||||||
|
scales = ()
|
||||||
|
|
||||||
|
def __init__(self, values, icc_profile=None, fallback=None):
|
||||||
|
if isinstance(values, str):
|
||||||
|
if values.strip().startswith("#") and " " in values:
|
||||||
|
fallback, values = values.split(" ", 1)
|
||||||
|
fallback = Color(fallback)
|
||||||
|
icc_profile, values = self.parse_css_color(values)
|
||||||
|
|
||||||
|
if icc_profile is None:
|
||||||
|
raise ColorError("CMS Color requires an icc color profile name.")
|
||||||
|
|
||||||
|
self.icc_profile = icc_profile
|
||||||
|
self.fallback_rgb = fallback
|
||||||
|
super().__init__(values)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
values = self.css_join.join([f"{v:g}" for v in self.get_css_values()])
|
||||||
|
fallback = str(ColorRGB(self.fallback_rgb)) + " " if self.fallback_rgb else ""
|
||||||
|
return f"{fallback}{self.css_func}({self.icc_profile}, {values})"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def can_parse(cls, string: str) -> bool:
|
||||||
|
# Custom detection because of RGB fallback prefix
|
||||||
|
return "icc-color" in string.replace("(", " ").split()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def constrain(cls, index, value):
|
||||||
|
return min(max(round_by_type(float, value), 0.0), 1.0)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def scale_up(cls, index, value):
|
||||||
|
return value # All cms values are already 0.0 to 1.0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def scale_down(cls, index, value):
|
||||||
|
return value # All cms values are already 0.0 to 1.0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_to_rgb(*data):
|
||||||
|
"""Catch attempted conversions to rgb"""
|
||||||
|
raise NotImplementedError("Can not convert to RGB from icc color")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_from_rgb(*data):
|
||||||
|
"""Catch attempted conversions from rgb"""
|
||||||
|
raise NotImplementedError("Can not convert from RGB to icc color")
|
||||||
|
|
||||||
|
|
||||||
|
# This is research code for a future developer to use. We already use PIL and this will
|
||||||
|
# allow icc colors to be converted in python. This isn't needed right now, so this work
|
||||||
|
# will be left undone.
|
||||||
|
# @staticmethod
|
||||||
|
# def convert_to_rgb():
|
||||||
|
# from PIL import Image, ImageCms
|
||||||
|
# pixel = Image.fromarray([[int(r * 255), int(g * 255), int(b * 255)]], 'RGB')
|
||||||
|
# transform = ImageCms.buildTransform(sRGB_profile, self.this_profile, "RGB",
|
||||||
|
# self.this_profile_mode, self.this_rendering_intent, 0)
|
||||||
|
# transform.apply_in_place(pixel)
|
||||||
|
# return [p / 255 for p in pixel[0]]
|
||||||
81
deps/inkex/colors/spaces/cmyk.py
vendored
Normal file
81
deps/inkex/colors/spaces/cmyk.py
vendored
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 Martin Owens
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# pylint: disable=W0223
|
||||||
|
"""
|
||||||
|
DeviceCMYK Color Space
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .css import CssColorModule4
|
||||||
|
|
||||||
|
|
||||||
|
class ColorDeviceCMYK(CssColorModule4):
|
||||||
|
"""
|
||||||
|
Parse the device-cmyk CSS Color Module 4 format.
|
||||||
|
|
||||||
|
Note that this format is NOT true CMYK as you might expect in a printer and
|
||||||
|
is instead is an aproximation of the intended ink levels if this was converted
|
||||||
|
into a real CMYK color profile using a color management system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "cmyk"
|
||||||
|
|
||||||
|
channels = ("cyan", "magenta", "yellow", "black")
|
||||||
|
scales = ((0, 100), (0, 100), (0, 100), (0, 100), (0.0, 1.0))
|
||||||
|
css_either_prefix = "device-cmyk"
|
||||||
|
|
||||||
|
cyan = property(
|
||||||
|
lambda self: self[0], lambda self, value: self.__setitem__(0, value)
|
||||||
|
)
|
||||||
|
magenta = property(
|
||||||
|
lambda self: self[1], lambda self, value: self.__setitem__(1, value)
|
||||||
|
)
|
||||||
|
yellow = property(
|
||||||
|
lambda self: self[2], lambda self, value: self.__setitem__(2, value)
|
||||||
|
)
|
||||||
|
black = property(
|
||||||
|
lambda self: self[3], lambda self, value: self.__setitem__(3, value)
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_to_rgb(cyan, magenta, yellow, black, *alpha):
|
||||||
|
"""
|
||||||
|
Convert a set of Device-CMYK identities into RGB
|
||||||
|
"""
|
||||||
|
white = 1.0 - black
|
||||||
|
return [
|
||||||
|
1.0 - min((1.0, cyan * white + black)),
|
||||||
|
1.0 - min((1.0, magenta * white + black)),
|
||||||
|
1.0 - min((1.0, yellow * white + black)),
|
||||||
|
] + list(alpha)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_from_rgb(red, green, blue, *alpha):
|
||||||
|
"""
|
||||||
|
Convert RGB into Device-CMYK
|
||||||
|
"""
|
||||||
|
white = max((red, green, blue))
|
||||||
|
black = 1.0 - white
|
||||||
|
return [
|
||||||
|
# Each channel is it's color chart oposite (cyan->red)
|
||||||
|
# with a bit of white removed.
|
||||||
|
(white and (1.0 - red - black) / white or 0.0),
|
||||||
|
(white and (1.0 - green - black) / white or 0.0),
|
||||||
|
(white and (1.0 - blue - black) / white or 0.0),
|
||||||
|
black,
|
||||||
|
] + list(alpha)
|
||||||
139
deps/inkex/colors/spaces/css.py
vendored
Normal file
139
deps/inkex/colors/spaces/css.py
vendored
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2018-2024 Martin Owens
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# pylint: disable=W0223
|
||||||
|
"""
|
||||||
|
Parsing CSS elements from colors
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from ..color import Color, ColorError, ColorIdError
|
||||||
|
|
||||||
|
|
||||||
|
class CssColor(Color):
|
||||||
|
"""
|
||||||
|
A Color which is always parsed and printed from a css format.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# A list of css prefixes which ar valid for this space
|
||||||
|
css_noalpha_prefix: Optional[str] = None
|
||||||
|
css_alpha_prefix: Optional[str] = None
|
||||||
|
css_either_prefix: Optional[str] = None
|
||||||
|
|
||||||
|
# Some CSS formats require commas, others do not
|
||||||
|
css_join: str = ", "
|
||||||
|
css_join_alpha: str = ", "
|
||||||
|
css_func = "color"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
values = self.css_join.join([f"{v:g}" for v in self.get_css_values()])
|
||||||
|
prefix = self.css_noalpha_prefix or self.css_either_prefix
|
||||||
|
if self.alpha is not None:
|
||||||
|
# Alpha is stored as a percent for clarity
|
||||||
|
alpha = int(self.alpha * 100)
|
||||||
|
values += self.css_join_alpha + f"{alpha}%"
|
||||||
|
if not self.css_either_prefix:
|
||||||
|
prefix = self.css_alpha_prefix
|
||||||
|
if prefix is None:
|
||||||
|
raise ColorError(f"Can't encode color {self.name} into CSS color format.")
|
||||||
|
return f"{prefix}({values})"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def can_parse(cls, string: str):
|
||||||
|
string = string.replace(" ", "")
|
||||||
|
if "(" not in string or ")" not in string:
|
||||||
|
return False
|
||||||
|
for prefix in (
|
||||||
|
cls.css_noalpha_prefix,
|
||||||
|
cls.css_alpha_prefix,
|
||||||
|
cls.css_either_prefix,
|
||||||
|
):
|
||||||
|
if prefix and (prefix + "(" in string or "color(" + prefix in string):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __init__(self, value, alpha=None):
|
||||||
|
if isinstance(value, str):
|
||||||
|
prefix, values = self.parse_css_color(value)
|
||||||
|
has_alpha = (
|
||||||
|
self.channels is not None and len(values) == len(self.channels) + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if prefix == self.css_noalpha_prefix or (
|
||||||
|
prefix == self.css_either_prefix and not has_alpha
|
||||||
|
):
|
||||||
|
super().__init__(values)
|
||||||
|
|
||||||
|
elif prefix == self.css_alpha_prefix or (
|
||||||
|
prefix == self.css_either_prefix and has_alpha
|
||||||
|
):
|
||||||
|
super().__init__(values, values.pop())
|
||||||
|
else:
|
||||||
|
raise ColorError(f"Could not parse {self.name} css color: '{value}'")
|
||||||
|
else:
|
||||||
|
super().__init__(value, alpha=alpha)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_css_color(cls, value):
|
||||||
|
"""Parse a css string into a list of values and it's color space prefix"""
|
||||||
|
prefix, values = value.lower().strip().strip(")").split("(")
|
||||||
|
# Some css formats use commas, others do not
|
||||||
|
if "," in cls.css_join:
|
||||||
|
values = values.replace(",", " ")
|
||||||
|
if "/" in cls.css_join_alpha:
|
||||||
|
values = values.replace("/", " ")
|
||||||
|
|
||||||
|
# Split values by spaces
|
||||||
|
values = values.split()
|
||||||
|
prefix = prefix.strip()
|
||||||
|
if prefix == cls.css_func:
|
||||||
|
prefix = values.pop(0)
|
||||||
|
if prefix == "url":
|
||||||
|
raise ColorIdError("Can not parse url as if it was a color.")
|
||||||
|
return prefix, [cls.parse_css_value(i, v) for i, v in enumerate(values)]
|
||||||
|
|
||||||
|
def get_css_values(self):
|
||||||
|
"""Return a list of values used for css string output"""
|
||||||
|
return self
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_css_value(cls, index, value) -> Union[int, float]:
|
||||||
|
"""Parse a CSS value such as 100%, 360 or 0.4"""
|
||||||
|
if cls.scales and index >= len(cls.scales):
|
||||||
|
raise ValueError("Can't add any more values to color.")
|
||||||
|
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = value.strip()
|
||||||
|
if value.endswith("%"):
|
||||||
|
value = float(value.strip("%")) / 100
|
||||||
|
elif "." in value:
|
||||||
|
value = float(value)
|
||||||
|
else:
|
||||||
|
value = int(value)
|
||||||
|
|
||||||
|
if isinstance(value, float) and value <= 1.0:
|
||||||
|
value = cls.scale_up(index, value)
|
||||||
|
return cls.constrain(index, value)
|
||||||
|
|
||||||
|
|
||||||
|
class CssColorModule4(CssColor):
|
||||||
|
"""Tweak the css parser for CSS Module Four formating"""
|
||||||
|
|
||||||
|
css_join = " "
|
||||||
|
css_join_alpha = " / "
|
||||||
107
deps/inkex/colors/spaces/hsl.py
vendored
Normal file
107
deps/inkex/colors/spaces/hsl.py
vendored
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 Martin Owens
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# pylint: disable=W0223
|
||||||
|
"""
|
||||||
|
HSL Color Space
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .css import CssColor
|
||||||
|
|
||||||
|
|
||||||
|
class ColorHSL(CssColor):
|
||||||
|
"""
|
||||||
|
Parse the HSL CSS Module Module 3 format.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "hsl"
|
||||||
|
channels = ("hue", "saturation", "lightness")
|
||||||
|
scales = ((0, 360, True), (0, 100), (0, 100), (0.0, 1.0))
|
||||||
|
|
||||||
|
css_noalpha_prefix = "hsl"
|
||||||
|
css_alpha_prefix = "hsla"
|
||||||
|
|
||||||
|
hue = property(lambda self: self[0], lambda self, value: self.__setitem__(0, value))
|
||||||
|
saturation = property(
|
||||||
|
lambda self: self[1], lambda self, value: self.__setitem__(1, value)
|
||||||
|
)
|
||||||
|
lightness = property(
|
||||||
|
lambda self: self[2], lambda self, value: self.__setitem__(2, value)
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_from_rgb(red, green, blue, alpha=None):
|
||||||
|
"""RGB to HSL colour conversion"""
|
||||||
|
rgb_max = max(red, green, blue)
|
||||||
|
rgb_min = min(red, green, blue)
|
||||||
|
delta = rgb_max - rgb_min
|
||||||
|
hsl = [0.0, 0.0, (rgb_max + rgb_min) / 2.0]
|
||||||
|
|
||||||
|
if delta != 0:
|
||||||
|
if hsl[2] <= 0.5:
|
||||||
|
hsl[1] = delta / (rgb_max + rgb_min)
|
||||||
|
else:
|
||||||
|
hsl[1] = delta / (2 - rgb_max - rgb_min)
|
||||||
|
|
||||||
|
if red == rgb_max:
|
||||||
|
hsl[0] = (green - blue) / delta
|
||||||
|
elif green == rgb_max:
|
||||||
|
hsl[0] = 2.0 + (blue - red) / delta
|
||||||
|
elif blue == rgb_max:
|
||||||
|
hsl[0] = 4.0 + (red - green) / delta
|
||||||
|
|
||||||
|
hsl[0] /= 6.0
|
||||||
|
if hsl[0] < 0:
|
||||||
|
hsl[0] += 1
|
||||||
|
if hsl[0] > 1:
|
||||||
|
hsl[0] -= 1
|
||||||
|
if alpha is not None:
|
||||||
|
hsl.append(alpha)
|
||||||
|
return hsl
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_to_rgb(hue, sat, light, *alpha):
|
||||||
|
"""HSL to RGB Color Conversion"""
|
||||||
|
if sat == 0:
|
||||||
|
return [light, light, light] # Gray
|
||||||
|
|
||||||
|
if light < 0.5:
|
||||||
|
val2 = light * (1 + sat)
|
||||||
|
else:
|
||||||
|
val2 = light + sat - light * sat
|
||||||
|
val1 = 2 * light - val2
|
||||||
|
ret = [
|
||||||
|
_hue_to_rgb(val1, val2, hue * 6 + 2.0),
|
||||||
|
_hue_to_rgb(val1, val2, hue * 6),
|
||||||
|
_hue_to_rgb(val1, val2, hue * 6 - 2.0),
|
||||||
|
]
|
||||||
|
return ret + list(alpha)
|
||||||
|
|
||||||
|
|
||||||
|
def _hue_to_rgb(val1, val2, hue):
|
||||||
|
if hue < 0:
|
||||||
|
hue += 6.0
|
||||||
|
if hue > 6:
|
||||||
|
hue -= 6.0
|
||||||
|
if hue < 1:
|
||||||
|
return val1 + (val2 - val1) * hue
|
||||||
|
if hue < 3:
|
||||||
|
return val2
|
||||||
|
if hue < 4:
|
||||||
|
return val1 + (val2 - val1) * (4 - hue)
|
||||||
|
return val1
|
||||||
88
deps/inkex/colors/spaces/hsv.py
vendored
Normal file
88
deps/inkex/colors/spaces/hsv.py
vendored
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 Jonathan Neuhauser
|
||||||
|
# 2024 Martin Owens
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# pylint: disable=W0223
|
||||||
|
"""
|
||||||
|
HSV Color Space
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .css import CssColorModule4
|
||||||
|
|
||||||
|
|
||||||
|
class ColorHSV(CssColorModule4):
|
||||||
|
"""
|
||||||
|
Parse the HWB CSS Color Module 4 format and retain as HSV values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "hsv"
|
||||||
|
channels = ("hue", "saturation", "value")
|
||||||
|
scales = ((0, 360, True), (0, 100), (0, 100), (0.0, 1.0))
|
||||||
|
|
||||||
|
# We use HWB to store HSV as this makes the most sense to Inkscape
|
||||||
|
css_either_prefix = "hwb"
|
||||||
|
|
||||||
|
hue = property(lambda self: self[0], lambda self, value: self.__setitem__(0, value))
|
||||||
|
saturation = property(
|
||||||
|
lambda self: self[1], lambda self, value: self.__setitem__(1, value)
|
||||||
|
)
|
||||||
|
value = property(
|
||||||
|
lambda self: self[2], lambda self, value: self.__setitem__(2, value)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_css_color(cls, value):
|
||||||
|
"""Parsing HWB as if it was HSV for css input"""
|
||||||
|
prefix, values = super().parse_css_color(value)
|
||||||
|
# See https://en.wikipedia.org/wiki/HWB_color_model#Converting_to_and_from_HSV
|
||||||
|
values[1] /= 100
|
||||||
|
values[2] /= 100
|
||||||
|
scale = values[1] + values[2]
|
||||||
|
if scale > 1.0:
|
||||||
|
values[1] /= scale
|
||||||
|
values[2] /= scale
|
||||||
|
values[1] = int(
|
||||||
|
(values[2] == 1.0 and 0.0 or (1.0 - (values[1] / (1.0 - values[2])))) * 100
|
||||||
|
)
|
||||||
|
values[2] = int((1.0 - values[2]) * 100)
|
||||||
|
return prefix, values
|
||||||
|
|
||||||
|
def get_css_values(self):
|
||||||
|
"""Convert our HSV values into HWB for css output"""
|
||||||
|
values = list(self)
|
||||||
|
values[1] = (100 - values[1]) * (values[2] / 100)
|
||||||
|
values[2] = 100 - values[2]
|
||||||
|
return values
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_to_hsl(hue, saturation, value, *alpha):
|
||||||
|
"""Conversion according to
|
||||||
|
https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_HSL
|
||||||
|
|
||||||
|
.. versionadded:: 1.5"""
|
||||||
|
lum = value * (1 - saturation / 2)
|
||||||
|
sat = 0 if lum in (0, 1) else (value - lum) / min(lum, 1 - lum)
|
||||||
|
return [hue, sat, lum] + list(alpha)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_from_hsl(hue, saturation, lightness, *alpha):
|
||||||
|
"""Convertion according to Inkscape C++ codebase
|
||||||
|
.. versionadded:: 1.5"""
|
||||||
|
val = lightness + saturation * min(lightness, 1 - lightness)
|
||||||
|
sat = 0 if val == 0 else 2 * (1 - lightness / val)
|
||||||
|
return [hue, sat, val] + list(alpha)
|
||||||
236
deps/inkex/colors/spaces/named.py
vendored
Normal file
236
deps/inkex/colors/spaces/named.py
vendored
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024, Martin Owens
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
CSS Named colors
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from ..color import Color
|
||||||
|
from .rgb import ColorRGB
|
||||||
|
|
||||||
|
_COLORS = {
|
||||||
|
"aliceblue": "#f0f8ff",
|
||||||
|
"antiquewhite": "#faebd7",
|
||||||
|
"aqua": "#00ffff",
|
||||||
|
"aquamarine": "#7fffd4",
|
||||||
|
"azure": "#f0ffff",
|
||||||
|
"beige": "#f5f5dc",
|
||||||
|
"bisque": "#ffe4c4",
|
||||||
|
"black": "#000000",
|
||||||
|
"blanchedalmond": "#ffebcd",
|
||||||
|
"blue": "#0000ff",
|
||||||
|
"blueviolet": "#8a2be2",
|
||||||
|
"brown": "#a52a2a",
|
||||||
|
"burlywood": "#deb887",
|
||||||
|
"cadetblue": "#5f9ea0",
|
||||||
|
"chartreuse": "#7fff00",
|
||||||
|
"chocolate": "#d2691e",
|
||||||
|
"coral": "#ff7f50",
|
||||||
|
"cornflowerblue": "#6495ed",
|
||||||
|
"cornsilk": "#fff8dc",
|
||||||
|
"crimson": "#dc143c",
|
||||||
|
"cyan": "#00ffff",
|
||||||
|
"darkblue": "#00008b",
|
||||||
|
"darkcyan": "#008b8b",
|
||||||
|
"darkgoldenrod": "#b8860b",
|
||||||
|
"darkgray": "#a9a9a9",
|
||||||
|
"darkgreen": "#006400",
|
||||||
|
"darkgrey": "#a9a9a9",
|
||||||
|
"darkkhaki": "#bdb76b",
|
||||||
|
"darkmagenta": "#8b008b",
|
||||||
|
"darkolivegreen": "#556b2f",
|
||||||
|
"darkorange": "#ff8c00",
|
||||||
|
"darkorchid": "#9932cc",
|
||||||
|
"darkred": "#8b0000",
|
||||||
|
"darksalmon": "#e9967a",
|
||||||
|
"darkseagreen": "#8fbc8f",
|
||||||
|
"darkslateblue": "#483d8b",
|
||||||
|
"darkslategray": "#2f4f4f",
|
||||||
|
"darkslategrey": "#2f4f4f",
|
||||||
|
"darkturquoise": "#00ced1",
|
||||||
|
"darkviolet": "#9400d3",
|
||||||
|
"deeppink": "#ff1493",
|
||||||
|
"deepskyblue": "#00bfff",
|
||||||
|
"dimgray": "#696969",
|
||||||
|
"dimgrey": "#696969",
|
||||||
|
"dodgerblue": "#1e90ff",
|
||||||
|
"firebrick": "#b22222",
|
||||||
|
"floralwhite": "#fffaf0",
|
||||||
|
"forestgreen": "#228b22",
|
||||||
|
"fuchsia": "#ff00ff",
|
||||||
|
"gainsboro": "#dcdcdc",
|
||||||
|
"ghostwhite": "#f8f8ff",
|
||||||
|
"gold": "#ffd700",
|
||||||
|
"goldenrod": "#daa520",
|
||||||
|
"gray": "#808080",
|
||||||
|
"grey": "#808080",
|
||||||
|
"green": "#008000",
|
||||||
|
"greenyellow": "#adff2f",
|
||||||
|
"honeydew": "#f0fff0",
|
||||||
|
"hotpink": "#ff69b4",
|
||||||
|
"indianred": "#cd5c5c",
|
||||||
|
"indigo": "#4b0082",
|
||||||
|
"ivory": "#fffff0",
|
||||||
|
"khaki": "#f0e68c",
|
||||||
|
"lavender": "#e6e6fa",
|
||||||
|
"lavenderblush": "#fff0f5",
|
||||||
|
"lawngreen": "#7cfc00",
|
||||||
|
"lemonchiffon": "#fffacd",
|
||||||
|
"lightblue": "#add8e6",
|
||||||
|
"lightcoral": "#f08080",
|
||||||
|
"lightcyan": "#e0ffff",
|
||||||
|
"lightgoldenrodyellow": "#fafad2",
|
||||||
|
"lightgray": "#d3d3d3",
|
||||||
|
"lightgreen": "#90ee90",
|
||||||
|
"lightgrey": "#d3d3d3",
|
||||||
|
"lightpink": "#ffb6c1",
|
||||||
|
"lightsalmon": "#ffa07a",
|
||||||
|
"lightseagreen": "#20b2aa",
|
||||||
|
"lightskyblue": "#87cefa",
|
||||||
|
"lightslategray": "#778899",
|
||||||
|
"lightslategrey": "#778899",
|
||||||
|
"lightsteelblue": "#b0c4de",
|
||||||
|
"lightyellow": "#ffffe0",
|
||||||
|
"lime": "#00ff00",
|
||||||
|
"limegreen": "#32cd32",
|
||||||
|
"linen": "#faf0e6",
|
||||||
|
"magenta": "#ff00ff",
|
||||||
|
"maroon": "#800000",
|
||||||
|
"mediumaquamarine": "#66cdaa",
|
||||||
|
"mediumblue": "#0000cd",
|
||||||
|
"mediumorchid": "#ba55d3",
|
||||||
|
"mediumpurple": "#9370db",
|
||||||
|
"mediumseagreen": "#3cb371",
|
||||||
|
"mediumslateblue": "#7b68ee",
|
||||||
|
"mediumspringgreen": "#00fa9a",
|
||||||
|
"mediumturquoise": "#48d1cc",
|
||||||
|
"mediumvioletred": "#c71585",
|
||||||
|
"midnightblue": "#191970",
|
||||||
|
"mintcream": "#f5fffa",
|
||||||
|
"mistyrose": "#ffe4e1",
|
||||||
|
"moccasin": "#ffe4b5",
|
||||||
|
"navajowhite": "#ffdead",
|
||||||
|
"navy": "#000080",
|
||||||
|
"oldlace": "#fdf5e6",
|
||||||
|
"olive": "#808000",
|
||||||
|
"olivedrab": "#6b8e23",
|
||||||
|
"orange": "#ffa500",
|
||||||
|
"orangered": "#ff4500",
|
||||||
|
"orchid": "#da70d6",
|
||||||
|
"palegoldenrod": "#eee8aa",
|
||||||
|
"palegreen": "#98fb98",
|
||||||
|
"paleturquoise": "#afeeee",
|
||||||
|
"palevioletred": "#db7093",
|
||||||
|
"papayawhip": "#ffefd5",
|
||||||
|
"peachpuff": "#ffdab9",
|
||||||
|
"peru": "#cd853f",
|
||||||
|
"pink": "#ffc0cb",
|
||||||
|
"plum": "#dda0dd",
|
||||||
|
"powderblue": "#b0e0e6",
|
||||||
|
"purple": "#800080",
|
||||||
|
"rebeccapurple": "#663399",
|
||||||
|
"red": "#ff0000",
|
||||||
|
"rosybrown": "#bc8f8f",
|
||||||
|
"royalblue": "#4169e1",
|
||||||
|
"saddlebrown": "#8b4513",
|
||||||
|
"salmon": "#fa8072",
|
||||||
|
"sandybrown": "#f4a460",
|
||||||
|
"seagreen": "#2e8b57",
|
||||||
|
"seashell": "#fff5ee",
|
||||||
|
"sienna": "#a0522d",
|
||||||
|
"silver": "#c0c0c0",
|
||||||
|
"skyblue": "#87ceeb",
|
||||||
|
"slateblue": "#6a5acd",
|
||||||
|
"slategray": "#708090",
|
||||||
|
"slategrey": "#708090",
|
||||||
|
"snow": "#fffafa",
|
||||||
|
"springgreen": "#00ff7f",
|
||||||
|
"steelblue": "#4682b4",
|
||||||
|
"tan": "#d2b48c",
|
||||||
|
"teal": "#008080",
|
||||||
|
"thistle": "#d8bfd8",
|
||||||
|
"tomato": "#ff6347",
|
||||||
|
"turquoise": "#40e0d0",
|
||||||
|
"violet": "#ee82ee",
|
||||||
|
"wheat": "#f5deb3",
|
||||||
|
"white": "#ffffff",
|
||||||
|
"whitesmoke": "#f5f5f5",
|
||||||
|
"yellow": "#ffff00",
|
||||||
|
"yellowgreen": "#9acd32",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ColorNamed(ColorRGB):
|
||||||
|
"""
|
||||||
|
Parse specific named colors, fall back to RGB parsing if it fails.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_color_names: Dict[ColorRGB, str] = {}
|
||||||
|
_name_colors: Dict[str, ColorRGB] = {}
|
||||||
|
|
||||||
|
name = "named"
|
||||||
|
|
||||||
|
def __init__(self, name, alpha=None):
|
||||||
|
if isinstance(name, str):
|
||||||
|
super().__init__(self.name_colors()[name.lower().strip()])
|
||||||
|
else:
|
||||||
|
super().__init__(name, alpha=alpha)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def color_names(cls):
|
||||||
|
"""Cache a list of color names"""
|
||||||
|
if not cls._color_names:
|
||||||
|
cls._color_names = {
|
||||||
|
value: name for name, value in cls.name_colors().items()
|
||||||
|
}
|
||||||
|
return cls._color_names
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def name_colors(cls):
|
||||||
|
"""Cache a list of color objects"""
|
||||||
|
if not cls._name_colors:
|
||||||
|
cls._name_colors = {name: Color(value) for name, value in _COLORS.items()}
|
||||||
|
return cls._name_colors
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.color_names().get(self, super().__str__())
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
"""Allow named colors to match rgb colors"""
|
||||||
|
return tuple(self + [self.alpha, super().name]).__hash__()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def can_parse(cls, string: str):
|
||||||
|
"""If the string is one of the color names, we can parse it"""
|
||||||
|
return string in cls.name_colors()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_to_rgb(*data):
|
||||||
|
"""Converting to RGB is transparent, already in RGB"""
|
||||||
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_from_rgb(*data):
|
||||||
|
"""Converting from RGB is transparent, the store is RGB"""
|
||||||
|
return data
|
||||||
|
|
||||||
|
def to_rgb(self):
|
||||||
|
"""Prevent masking by ColorRGB of to_rgb method"""
|
||||||
|
return ColorRGB(list(self), alpha=self.alpha)
|
||||||
55
deps/inkex/colors/spaces/none.py
vendored
Normal file
55
deps/inkex/colors/spaces/none.py
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2021 Jonathan Neuhauser
|
||||||
|
# 2020 Martin Owens
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# pylint: disable=W0223
|
||||||
|
"""
|
||||||
|
An empty color for 'none'
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ..color import Color, AlphaNotAllowed
|
||||||
|
|
||||||
|
|
||||||
|
class ColorNone(Color, AlphaNotAllowed):
|
||||||
|
"""A special color for 'none' colors"""
|
||||||
|
|
||||||
|
name = "none"
|
||||||
|
|
||||||
|
# Override opacity since none can not have opacity
|
||||||
|
default_alpha = 0.0
|
||||||
|
|
||||||
|
def __init__(self, value=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return "none"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def can_parse(cls, string: str) -> bool:
|
||||||
|
"""Returns true if this is the word 'none'"""
|
||||||
|
return string == "none"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_to_rgb(*_):
|
||||||
|
"""Converting to RGB means transparent black"""
|
||||||
|
return [0, 0, 0, 0]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_from_rgb(*_):
|
||||||
|
"""Converting from RGB means throwing out all data"""
|
||||||
|
return []
|
||||||
105
deps/inkex/colors/spaces/rgb.py
vendored
Normal file
105
deps/inkex/colors/spaces/rgb.py
vendored
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024, Martin Owens
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
RGB Colors
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ..color import ColorError
|
||||||
|
from .css import CssColor
|
||||||
|
|
||||||
|
|
||||||
|
class ColorRGB(CssColor):
|
||||||
|
"""
|
||||||
|
Parse multiple versions of RGB from CSS module and standard hex formats.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "rgb"
|
||||||
|
channels = ("red", "green", "blue")
|
||||||
|
scales = ((0, 255), (0, 255), (0, 255), (0.0, 1.0))
|
||||||
|
|
||||||
|
css_noalpha_prefix = "rgb"
|
||||||
|
css_alpha_prefix = "rgba"
|
||||||
|
|
||||||
|
red = property(lambda self: self[0], lambda self, value: self.__setitem__(0, value))
|
||||||
|
green = property(
|
||||||
|
lambda self: self[1], lambda self, value: self.__setitem__(1, value)
|
||||||
|
)
|
||||||
|
blue = property(
|
||||||
|
lambda self: self[2], lambda self, value: self.__setitem__(2, value)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def can_parse(cls, string: str) -> bool:
|
||||||
|
return "icc" not in string and (
|
||||||
|
string.startswith("#")
|
||||||
|
or string.lstrip("-").isdigit()
|
||||||
|
or super().can_parse(string)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, value, alpha=None):
|
||||||
|
# Not CSS, but inkscape, some old color values stores as 32bit int strings
|
||||||
|
if isinstance(value, str) and value.lstrip("-").isdigit():
|
||||||
|
value = int(value)
|
||||||
|
|
||||||
|
if isinstance(value, int):
|
||||||
|
super().__init__(
|
||||||
|
[
|
||||||
|
((value >> 24) & 255), # red
|
||||||
|
((value >> 16) & 255), # green
|
||||||
|
((value >> 8) & 255), # blue
|
||||||
|
((value & 255) / 255.0),
|
||||||
|
]
|
||||||
|
) # opacity
|
||||||
|
|
||||||
|
elif isinstance(value, str) and value.startswith("#") and " " not in value:
|
||||||
|
if len(value) == 4: # (css: #rgb -> #rrggbb)
|
||||||
|
# pylint: disable=consider-using-f-string
|
||||||
|
value = "#{1}{1}{2}{2}{3}{3}".format(*value)
|
||||||
|
elif len(value) == 5: # (css: #rgba -> #rrggbbaa)
|
||||||
|
# pylint: disable=consider-using-f-string
|
||||||
|
value = "#{1}{1}{2}{2}{3}{3}{4}{4}".format(*value)
|
||||||
|
|
||||||
|
# Convert hex to integers
|
||||||
|
try:
|
||||||
|
values = [int(value[i : i + 2], 16) for i in range(1, len(value), 2)]
|
||||||
|
if len(values) == 4:
|
||||||
|
values[3] /= 255
|
||||||
|
super().__init__(values)
|
||||||
|
except ValueError as error:
|
||||||
|
raise ColorError(f"Bad RGB hex color value '{value}'") from error
|
||||||
|
else:
|
||||||
|
super().__init__(value, alpha=alpha)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
if self.alpha is not None:
|
||||||
|
return super().__str__()
|
||||||
|
if len(self) < len(self.channels):
|
||||||
|
raise ColorError(
|
||||||
|
f"Incorrect number of channels for Color Space {self.name}"
|
||||||
|
)
|
||||||
|
# Always hex values when outputting color
|
||||||
|
return "#{0:02x}{1:02x}{2:02x}".format(*(int(v) for v in self)) # pylint: disable=consider-using-f-string
|
||||||
|
|
||||||
|
def __int__(self) -> int:
|
||||||
|
return (
|
||||||
|
(self[0] << 24)
|
||||||
|
+ (self[1] << 16)
|
||||||
|
+ (self[2] << 8)
|
||||||
|
+ int((self.alpha or 1.0) * 255)
|
||||||
|
)
|
||||||
31
deps/inkex/colors/utils.py
vendored
Normal file
31
deps/inkex/colors/utils.py
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2018-2024 Martin Owens
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Utilities for color support
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .color import Color, ColorError
|
||||||
|
|
||||||
|
|
||||||
|
def is_color(color):
|
||||||
|
"""Determine if it is a color that we can use. If not, leave it unchanged."""
|
||||||
|
try:
|
||||||
|
return bool(Color(color))
|
||||||
|
except ColorError:
|
||||||
|
return False
|
||||||
347
deps/inkex/command.py
vendored
Normal file
347
deps/inkex/command.py
vendored
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2019 Martin Owens
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110, USA.
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
This API provides methods for calling Inkscape to execute a given
|
||||||
|
Inkscape command. This may be needed for various compiling options
|
||||||
|
(e.g., png), running other extensions or performing other options only
|
||||||
|
available via the shell API.
|
||||||
|
|
||||||
|
Best practice is to avoid using this API except when absolutely necessary,
|
||||||
|
since it is resource-intensive to invoke a new Inkscape instance.
|
||||||
|
|
||||||
|
However, in any circumstance when it is necessary to call Inkscape, it
|
||||||
|
is strongly recommended that you do so through this API, rather than calling
|
||||||
|
it yourself, to take advantage of the security settings and testing functions.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from shutil import which as warlock
|
||||||
|
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
from typing import List
|
||||||
|
from lxml.etree import ElementTree
|
||||||
|
|
||||||
|
from .elements import SvgDocumentElement
|
||||||
|
|
||||||
|
INKSCAPE_EXECUTABLE_NAME = os.environ.get("INKSCAPE_COMMAND")
|
||||||
|
if INKSCAPE_EXECUTABLE_NAME is None:
|
||||||
|
if sys.platform == "win32":
|
||||||
|
# prefer inkscape.exe over inkscape.com which spawns a command window
|
||||||
|
INKSCAPE_EXECUTABLE_NAME = "inkscape.exe"
|
||||||
|
else:
|
||||||
|
INKSCAPE_EXECUTABLE_NAME = "inkscape"
|
||||||
|
|
||||||
|
|
||||||
|
class CommandNotFound(IOError):
|
||||||
|
"""Command is not found"""
|
||||||
|
|
||||||
|
|
||||||
|
class ProgramRunError(ValueError):
|
||||||
|
"""A specialized ValueError that is raised when a call to an external command fails.
|
||||||
|
It stores additional information about a failed call to an external program.
|
||||||
|
|
||||||
|
If only the ``program`` parameter is given, it is interpreted as the error message.
|
||||||
|
Otherwise, the error message is compiled from all constructor parameters."""
|
||||||
|
|
||||||
|
program: str
|
||||||
|
"""The absolute path to the called executable"""
|
||||||
|
|
||||||
|
returncode: int
|
||||||
|
"""Return code of the program call"""
|
||||||
|
|
||||||
|
stderr: str
|
||||||
|
"""stderr stream output of the call"""
|
||||||
|
|
||||||
|
stdout: str
|
||||||
|
"""stdout stream output of the call"""
|
||||||
|
|
||||||
|
arguments: List
|
||||||
|
"""Arguments of the call"""
|
||||||
|
|
||||||
|
def __init__(self, program, returncode=None, stderr=None, stdout=None, args=None):
|
||||||
|
self.program = program
|
||||||
|
self.returncode = returncode
|
||||||
|
self.stderr = stderr
|
||||||
|
self.stdout = stdout
|
||||||
|
self.arguments = args
|
||||||
|
super().__init__(str(self))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.returncode is None:
|
||||||
|
return self.program
|
||||||
|
return (
|
||||||
|
f"Return Code: {self.returncode}: {self.stderr}\n{self.stdout}"
|
||||||
|
f"\nargs: {self.args}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def which(program):
|
||||||
|
"""
|
||||||
|
Attempt different methods of trying to find if the program exists.
|
||||||
|
"""
|
||||||
|
if os.path.isabs(program) and os.path.isfile(program):
|
||||||
|
return program
|
||||||
|
# On Windows, shutil.which may give preference to .py files in the current directory
|
||||||
|
# (such as pdflatex.py), e.g. if .PY is in pathext, because the current directory is
|
||||||
|
# prepended to PATH. This can be suppressed by explicitly appending the current
|
||||||
|
# directory.
|
||||||
|
|
||||||
|
try:
|
||||||
|
if sys.platform == "win32":
|
||||||
|
prog = warlock(program, path=os.environ["PATH"] + ";" + os.curdir)
|
||||||
|
if prog:
|
||||||
|
return prog
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Python3 only version of which
|
||||||
|
prog = warlock(program)
|
||||||
|
if prog:
|
||||||
|
return prog
|
||||||
|
except ImportError:
|
||||||
|
pass # python2
|
||||||
|
|
||||||
|
# There may be other methods for doing a `which` command for other
|
||||||
|
# operating systems; These should go here as they are discovered.
|
||||||
|
|
||||||
|
raise CommandNotFound(f"Can not find the command: '{program}'")
|
||||||
|
|
||||||
|
|
||||||
|
def write_svg(svg, *filename):
|
||||||
|
"""Writes an svg to the given filename"""
|
||||||
|
filename = os.path.join(*filename)
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
return filename
|
||||||
|
with open(filename, "wb") as fhl:
|
||||||
|
if isinstance(svg, SvgDocumentElement):
|
||||||
|
svg = ElementTree(svg)
|
||||||
|
if hasattr(svg, "write"):
|
||||||
|
# XML document
|
||||||
|
svg.write(fhl)
|
||||||
|
elif isinstance(svg, bytes):
|
||||||
|
fhl.write(svg)
|
||||||
|
else:
|
||||||
|
raise ValueError("Not sure what type of SVG data this is.")
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
|
def to_arg(arg, oldie=False):
|
||||||
|
"""Convert a python argument to a command line argument"""
|
||||||
|
if isinstance(arg, (tuple, list)):
|
||||||
|
(arg, val) = arg
|
||||||
|
arg = "-" + arg
|
||||||
|
if len(arg) > 2 and not oldie:
|
||||||
|
arg = "-" + arg
|
||||||
|
if val is True:
|
||||||
|
return arg
|
||||||
|
if val is False:
|
||||||
|
return None
|
||||||
|
return f"{arg}={str(val)}"
|
||||||
|
return str(arg)
|
||||||
|
|
||||||
|
|
||||||
|
def to_args(prog, *positionals, **arguments):
|
||||||
|
"""Compile arguments and keyword arguments into a list of strings which Popen will
|
||||||
|
understand.
|
||||||
|
|
||||||
|
:param prog:
|
||||||
|
Program executable prepended to the output.
|
||||||
|
:type first: ``str``
|
||||||
|
|
||||||
|
:Arguments:
|
||||||
|
* (``str``) -- String added as given
|
||||||
|
* (``tuple``) -- Ordered version of Keyword Arguments, see below
|
||||||
|
|
||||||
|
:Keyword Arguments:
|
||||||
|
* *name* (``str``) --
|
||||||
|
Becomes ``--name="val"``
|
||||||
|
* *name* (``bool``) --
|
||||||
|
Becomes ``--name``
|
||||||
|
* *name* (``list``) --
|
||||||
|
Becomes ``--name="val1"`` ...
|
||||||
|
* *n* (``str``) --
|
||||||
|
Becomes ``-n=val``
|
||||||
|
* *n* (``bool``) --
|
||||||
|
Becomes ``-n``
|
||||||
|
|
||||||
|
:return: Returns a list of compiled arguments ready for Popen.
|
||||||
|
:rtype: ``list[str]``
|
||||||
|
"""
|
||||||
|
args = [prog]
|
||||||
|
oldie = arguments.pop("oldie", False)
|
||||||
|
for arg, value in arguments.items():
|
||||||
|
arg = arg.replace("_", "-").strip()
|
||||||
|
|
||||||
|
if isinstance(value, tuple):
|
||||||
|
value = list(value)
|
||||||
|
elif not isinstance(value, list):
|
||||||
|
value = [value]
|
||||||
|
|
||||||
|
for val in value:
|
||||||
|
args.append(to_arg((arg, val), oldie))
|
||||||
|
|
||||||
|
args += [to_arg(pos, oldie) for pos in positionals if pos is not None]
|
||||||
|
# Filter out empty non-arguments
|
||||||
|
return [arg for arg in args if arg is not None]
|
||||||
|
|
||||||
|
|
||||||
|
def to_args_sorted(prog, *positionals, **arguments):
|
||||||
|
"""same as :func:`to_args`, but keyword arguments are sorted beforehand
|
||||||
|
|
||||||
|
.. versionadded:: 1.2"""
|
||||||
|
return to_args(prog, *positionals, **dict(sorted(arguments.items())))
|
||||||
|
|
||||||
|
|
||||||
|
def _call(program, *args, **kwargs):
|
||||||
|
stdin = kwargs.pop("stdin", None)
|
||||||
|
if isinstance(stdin, str):
|
||||||
|
stdin = stdin.encode("utf-8")
|
||||||
|
inpipe = PIPE if stdin else None
|
||||||
|
|
||||||
|
args = to_args(which(program), *args, **kwargs)
|
||||||
|
|
||||||
|
kwargs = {}
|
||||||
|
if sys.platform == "win32":
|
||||||
|
kwargs["creationflags"] = 0x08000000 # create no console window
|
||||||
|
|
||||||
|
with Popen(
|
||||||
|
args,
|
||||||
|
shell=False, # Never have shell=True
|
||||||
|
stdin=inpipe, # StdIn not used (yet)
|
||||||
|
stdout=PIPE, # Grab any output (return it)
|
||||||
|
stderr=PIPE, # Take all errors, just incase
|
||||||
|
**kwargs,
|
||||||
|
) as process:
|
||||||
|
(stdout, stderr) = process.communicate(input=stdin)
|
||||||
|
if process.returncode == 0:
|
||||||
|
return stdout
|
||||||
|
raise ProgramRunError(program, process.returncode, stderr, stdout, args)
|
||||||
|
|
||||||
|
|
||||||
|
def call(program, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Generic caller to open any program and return its stdout::
|
||||||
|
|
||||||
|
stdout = call('executable', arg1, arg2, dash_dash_arg='foo', d=True, ...)
|
||||||
|
|
||||||
|
Will raise :class:`ProgramRunError` if return code is not 0.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
return_binary: Should stdout return raw bytes (default: False)
|
||||||
|
|
||||||
|
.. versionadded:: 1.1
|
||||||
|
stdin: The string or bytes containing the stdin (default: None)
|
||||||
|
|
||||||
|
All other arguments converted using :func:`to_args` function.
|
||||||
|
"""
|
||||||
|
# We use this long input because it's less likely to conflict with --binary=
|
||||||
|
binary = kwargs.pop("return_binary", False)
|
||||||
|
stdout = _call(program, *args, **kwargs)
|
||||||
|
# Convert binary to string when we wish to have strings we do this here
|
||||||
|
# so the mock tests will also run the conversion (always returns bytes)
|
||||||
|
if not binary and isinstance(stdout, bytes):
|
||||||
|
return stdout.decode(sys.stdout.encoding or "utf-8")
|
||||||
|
return stdout
|
||||||
|
|
||||||
|
|
||||||
|
def inkscape(svg_file, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Call Inkscape with the given svg_file and the given arguments, see call().
|
||||||
|
|
||||||
|
Returns the stdout of the call.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.3
|
||||||
|
If the "actions" kwargs parameter is passed, it is checked whether the length of
|
||||||
|
the action string might lead to issues with the Windows CLI call character
|
||||||
|
limit. In this case, Inkscape is called in `--shell`
|
||||||
|
mode and the actions are fed in via stdin. This avoids violating the character
|
||||||
|
limit for command line arguments on Windows, which results in errors like this:
|
||||||
|
`[WinError 206] The filename or extension is too long`.
|
||||||
|
This workaround is also possible when calling Inkscape with long arguments
|
||||||
|
to `--export-id` and `--query-id`, by converting the call to the appropriate
|
||||||
|
action sequence. The stdout is cleaned to resemble non-interactive mode.
|
||||||
|
"""
|
||||||
|
os.environ["SELF_CALL"] = "true"
|
||||||
|
actions = kwargs.get("actions", None)
|
||||||
|
strip_stdout = False
|
||||||
|
# Keep some safe margin to the 8191 character limit.
|
||||||
|
if actions is not None and len(actions) > 7000:
|
||||||
|
args = args + ("--shell",)
|
||||||
|
kwargs["stdin"] = actions
|
||||||
|
kwargs.pop("actions")
|
||||||
|
strip_stdout = True
|
||||||
|
stdout = call(INKSCAPE_EXECUTABLE_NAME, svg_file, *args, **kwargs)
|
||||||
|
if strip_stdout:
|
||||||
|
split = re.split(r"\n> ", stdout)
|
||||||
|
if len(split) > 1:
|
||||||
|
if "\n" in split[1]:
|
||||||
|
stdout = "\n".join(split[1].split("\n")[1:])
|
||||||
|
else:
|
||||||
|
stdout = ""
|
||||||
|
return stdout
|
||||||
|
|
||||||
|
|
||||||
|
def inkscape_command(svg, select=None, actions=None, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Executes Inkscape batch actions with the given <svg> input and returns a new <svg>.
|
||||||
|
|
||||||
|
inkscape_command('<svg...>', [select=...], [actions=...], [...])
|
||||||
|
"""
|
||||||
|
with TemporaryDirectory(prefix="inkscape-command") as tmpdir:
|
||||||
|
svg_file = write_svg(svg, tmpdir, "input.svg")
|
||||||
|
select = ("select", select) if select else None
|
||||||
|
inkscape(
|
||||||
|
svg_file,
|
||||||
|
select,
|
||||||
|
batch_process=True,
|
||||||
|
export_overwrite=True,
|
||||||
|
actions=actions,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
with open(svg_file, "rb") as fhl:
|
||||||
|
return fhl.read()
|
||||||
|
|
||||||
|
|
||||||
|
def take_snapshot(svg, dirname, name="snapshot", ext="png", dpi=96, **kwargs):
|
||||||
|
"""
|
||||||
|
Take a snapshot of the given svg file.
|
||||||
|
|
||||||
|
Resulting filename is yielded back, after generator finishes, the
|
||||||
|
file is deleted so you must deal with the file inside the for loop.
|
||||||
|
"""
|
||||||
|
svg_file = write_svg(svg, dirname, name + ".svg")
|
||||||
|
ext_file = os.path.join(dirname, name + "." + str(ext).lower())
|
||||||
|
inkscape(
|
||||||
|
svg_file, export_dpi=dpi, export_filename=ext_file, export_type=ext, **kwargs
|
||||||
|
)
|
||||||
|
return ext_file
|
||||||
|
|
||||||
|
|
||||||
|
def is_inkscape_available():
|
||||||
|
"""Return true if the Inkscape executable is available."""
|
||||||
|
try:
|
||||||
|
return bool(which(INKSCAPE_EXECUTABLE_NAME))
|
||||||
|
except CommandNotFound:
|
||||||
|
return False
|
||||||
3
deps/inkex/css/__init__.py
vendored
Normal file
3
deps/inkex/css/__init__.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""CSS Processing module"""
|
||||||
|
|
||||||
|
from .compiler import CSSCompiler
|
||||||
BIN
deps/inkex/css/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/css/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/css/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/css/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/css/__pycache__/compiler.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/css/__pycache__/compiler.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/css/__pycache__/compiler.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/css/__pycache__/compiler.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/css/__pycache__/parser.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/css/__pycache__/parser.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/css/__pycache__/parser.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/css/__pycache__/parser.cpython-313.pyc
vendored
Normal file
Binary file not shown.
483
deps/inkex/css/compiler.py
vendored
Normal file
483
deps/inkex/css/compiler.py
vendored
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2023 - Jonathan Neuhauser <jonathan.neuhauser@outlook.com>
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""CSS evaluation logic, forked from cssselect2 (rewritten without eval, targeted to
|
||||||
|
our data structure). CSS selectors are compiled into boolean evaluator functions.
|
||||||
|
All HTML-specific code has been removed, and we don't duplicate the tree data structure
|
||||||
|
but work on the normal tree."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from lxml import etree
|
||||||
|
from typing import Union, List
|
||||||
|
from tinycss2.nth import parse_nth
|
||||||
|
|
||||||
|
from . import parser
|
||||||
|
from .parser import SelectorError
|
||||||
|
|
||||||
|
# http://dev.w3.org/csswg/selectors/#whitespace
|
||||||
|
split_whitespace = re.compile("[^ \t\r\n\f]+").findall
|
||||||
|
|
||||||
|
|
||||||
|
def ascii_lower(string): # from webencodings
|
||||||
|
r"""Transform (only) ASCII letters to lower case: A-Z is mapped to a-z."""
|
||||||
|
return string.encode("utf8").lower().decode("utf8")
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=protected-access,comparison-with-callable,invalid-name,bad-super-call
|
||||||
|
# pylint: disable=unnecessary-lambda-assignment
|
||||||
|
|
||||||
|
|
||||||
|
## Iterators without comments.
|
||||||
|
def iterancestors(element):
|
||||||
|
"""Iterate over ancestors but ignore comments."""
|
||||||
|
for e in element.iterancestors():
|
||||||
|
if isinstance(e, etree._Comment):
|
||||||
|
continue
|
||||||
|
yield e
|
||||||
|
|
||||||
|
|
||||||
|
def iterdescendants(element):
|
||||||
|
"""Iterate over descendants but ignore comments"""
|
||||||
|
for e in element.iterdescendants():
|
||||||
|
if isinstance(e, etree._Comment):
|
||||||
|
continue
|
||||||
|
yield e
|
||||||
|
|
||||||
|
|
||||||
|
def itersiblings(element, preceding=False):
|
||||||
|
"""Iterate over descendants but ignore comments"""
|
||||||
|
for e in element.itersiblings(preceding=preceding):
|
||||||
|
if isinstance(e, etree._Comment):
|
||||||
|
continue
|
||||||
|
yield e
|
||||||
|
|
||||||
|
|
||||||
|
def iterchildren(element):
|
||||||
|
"""Iterate over children but ignore comments"""
|
||||||
|
for e in element.iterchildren():
|
||||||
|
if isinstance(e, etree._Comment):
|
||||||
|
continue
|
||||||
|
yield e
|
||||||
|
|
||||||
|
|
||||||
|
def getprevious(element):
|
||||||
|
"""Get the previous non-comment element"""
|
||||||
|
for e in itersiblings(element, preceding=True):
|
||||||
|
return e
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def getnext(element):
|
||||||
|
"""Get the next non-comment element"""
|
||||||
|
for e in itersiblings(element, preceding=False):
|
||||||
|
return e
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def FALSE(_el):
|
||||||
|
"""Always returns 0"""
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def TRUE(_el):
|
||||||
|
"""Always returns 1"""
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
class BooleanCompiler:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._func_map = {
|
||||||
|
parser.CombinedSelector: self._compile_combined,
|
||||||
|
parser.CompoundSelector: self._compile_compound,
|
||||||
|
parser.NegationSelector: self._compile_negation,
|
||||||
|
parser.RelationalSelector: self._compile_relational,
|
||||||
|
parser.MatchesAnySelector: self._compile_any,
|
||||||
|
parser.SpecificityAdjustmentSelector: self._compile_any,
|
||||||
|
parser.LocalNameSelector: self._compile_local_name,
|
||||||
|
parser.NamespaceSelector: self._compile_namespace,
|
||||||
|
parser.ClassSelector: self._compile_class,
|
||||||
|
parser.IDSelector: self._compile_id,
|
||||||
|
parser.AttributeSelector: self._compile_attribute,
|
||||||
|
parser.PseudoClassSelector: self._compile_pseudoclass,
|
||||||
|
parser.FunctionalPseudoClassSelector: self._compile_functional_pseudoclass,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _compile_combined(self, selector: parser.CombinedSelector):
|
||||||
|
left_inside = self.compile_node(selector.left)
|
||||||
|
if left_inside == FALSE:
|
||||||
|
return FALSE # 0 and x == 0
|
||||||
|
if left_inside == TRUE:
|
||||||
|
# 1 and x == x, but the element matching 1 still needs to exist.
|
||||||
|
if selector.combinator in (" ", ">"):
|
||||||
|
left = lambda el: el.getparent() is not None
|
||||||
|
|
||||||
|
elif selector.combinator in ("~", "+"):
|
||||||
|
left = lambda el: getprevious(el) is not None
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise SelectorError("Unknown combinator", selector.combinator)
|
||||||
|
elif selector.combinator == " ":
|
||||||
|
left = lambda el: any((left_inside(e)) for e in el.ancestors())
|
||||||
|
|
||||||
|
elif selector.combinator == ">":
|
||||||
|
left = lambda el: el.getparent() is not None and left_inside(el.getparent())
|
||||||
|
|
||||||
|
elif selector.combinator == "+":
|
||||||
|
left = lambda el: getprevious(el) is not None and left_inside(
|
||||||
|
getprevious(el)
|
||||||
|
)
|
||||||
|
|
||||||
|
elif selector.combinator == "~":
|
||||||
|
left = lambda el: any(
|
||||||
|
(left_inside(e)) for e in itersiblings(el, preceding=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise SelectorError("Unknown combinator", selector.combinator)
|
||||||
|
|
||||||
|
right = self.compile_node(selector.right)
|
||||||
|
if right == FALSE:
|
||||||
|
return FALSE # 0 and x == 0
|
||||||
|
if right == TRUE:
|
||||||
|
return left # 1 and x == x
|
||||||
|
# Evaluate combinators right to left
|
||||||
|
return lambda el: right(el) and left(el)
|
||||||
|
|
||||||
|
def _compile_compound(self, selector: parser.CompoundSelector):
|
||||||
|
sub_expressions = [
|
||||||
|
expr
|
||||||
|
for expr in map(self.compile_node, selector.simple_selectors)
|
||||||
|
if expr != TRUE
|
||||||
|
]
|
||||||
|
if len(sub_expressions) == 1:
|
||||||
|
return sub_expressions[0]
|
||||||
|
if FALSE in sub_expressions:
|
||||||
|
return FALSE
|
||||||
|
if sub_expressions:
|
||||||
|
return lambda e: all(expr(e) for expr in sub_expressions)
|
||||||
|
return TRUE # all([]) == True
|
||||||
|
|
||||||
|
def _compile_negation(self, selector: parser.NegationSelector):
|
||||||
|
sub_expressions = [
|
||||||
|
expr
|
||||||
|
for expr in [
|
||||||
|
self.compile_node(selector.parsed_tree)
|
||||||
|
for selector in selector.selector_list
|
||||||
|
]
|
||||||
|
if expr != TRUE
|
||||||
|
]
|
||||||
|
if not sub_expressions:
|
||||||
|
return FALSE
|
||||||
|
return lambda el: not any(expr(el) for expr in sub_expressions)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_subexpr(expression, relative_selector):
|
||||||
|
"""Helper function for RelationalSelector"""
|
||||||
|
if relative_selector.combinator == " ":
|
||||||
|
return lambda el: any(expression(e) for e in iterdescendants(el))
|
||||||
|
if relative_selector.combinator == ">":
|
||||||
|
return lambda el: any(expression(e) for e in iterchildren(el))
|
||||||
|
if relative_selector.combinator == "+":
|
||||||
|
return lambda el: expression(next(itersiblings(el)))
|
||||||
|
if relative_selector.combinator == "~":
|
||||||
|
return lambda el: any(expression(e) for e in itersiblings(el))
|
||||||
|
raise SelectorError(
|
||||||
|
f"Unknown relational selector '{relative_selector.combinator}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compile_relational(self, selector: parser.RelationalSelector):
|
||||||
|
sub_expr = []
|
||||||
|
|
||||||
|
for relative_selector in selector.selector_list:
|
||||||
|
expression = self.compile_node(relative_selector.selector.parsed_tree)
|
||||||
|
if expression == FALSE:
|
||||||
|
continue
|
||||||
|
sub_expr.append(self._get_subexpr(expression, relative_selector))
|
||||||
|
return lambda el: any(expr(el) for expr in sub_expr)
|
||||||
|
|
||||||
|
def _compile_any(
|
||||||
|
self,
|
||||||
|
selector: Union[
|
||||||
|
parser.MatchesAnySelector, parser.SpecificityAdjustmentSelector
|
||||||
|
],
|
||||||
|
):
|
||||||
|
sub_expressions = [
|
||||||
|
expr
|
||||||
|
for expr in [
|
||||||
|
self.compile_node(selector.parsed_tree)
|
||||||
|
for selector in selector.selector_list
|
||||||
|
]
|
||||||
|
if expr != FALSE
|
||||||
|
]
|
||||||
|
if not sub_expressions:
|
||||||
|
return FALSE
|
||||||
|
return lambda el: any(expr(el) for expr in sub_expressions)
|
||||||
|
|
||||||
|
def _compile_local_name(self, selector: parser.LocalNameSelector):
|
||||||
|
return lambda el: el.TAG == selector.local_name
|
||||||
|
|
||||||
|
def _compile_namespace(self, selector: parser.NamespaceSelector):
|
||||||
|
return lambda el: el.NAMESPACE == selector.namespace
|
||||||
|
|
||||||
|
def _compile_class(self, selector: parser.ClassSelector):
|
||||||
|
return lambda el: selector.class_name in el.classes
|
||||||
|
|
||||||
|
def _compile_id(self, selector: parser.IDSelector):
|
||||||
|
return lambda el: super(etree.ElementBase, el).get("id", None) == selector.ident # type: ignore
|
||||||
|
|
||||||
|
def _compile_attribute(self, selector: parser.AttributeSelector):
|
||||||
|
if selector.namespace is not None:
|
||||||
|
if selector.namespace:
|
||||||
|
key_func = lambda el: (
|
||||||
|
f"{{{selector.namespace}}}{selector.name}"
|
||||||
|
if el.NAMESPACE != selector.namespace
|
||||||
|
else selector.name
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
key_func = lambda el: selector.name
|
||||||
|
|
||||||
|
value = selector.value
|
||||||
|
if selector.case_sensitive is False:
|
||||||
|
value = value.lower()
|
||||||
|
|
||||||
|
attribute_value = (
|
||||||
|
lambda el: super(etree.ElementBase, el)
|
||||||
|
.get(key_func(el), "") # type: ignore
|
||||||
|
.lower()
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
attribute_value = lambda el: super(etree.ElementBase, el).get( # type: ignore
|
||||||
|
key_func(el), ""
|
||||||
|
)
|
||||||
|
|
||||||
|
if selector.operator is None:
|
||||||
|
return lambda el: key_func(el) in el.attrib
|
||||||
|
if selector.operator == "=":
|
||||||
|
return lambda el: (
|
||||||
|
key_func(el) in el.attrib and attribute_value(el) == value
|
||||||
|
)
|
||||||
|
if selector.operator == "~=":
|
||||||
|
return (
|
||||||
|
FALSE
|
||||||
|
if len(value.split()) != 1 or value.strip() != value
|
||||||
|
else lambda el: value in split_whitespace(attribute_value(el))
|
||||||
|
)
|
||||||
|
if selector.operator == "|=":
|
||||||
|
return lambda el: (
|
||||||
|
key_func(el) in el.attrib
|
||||||
|
and (
|
||||||
|
attribute_value(el) == value
|
||||||
|
or attribute_value(el).startswith(value + "-")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if selector.operator == "^=":
|
||||||
|
if value:
|
||||||
|
return lambda el: attribute_value(el).startswith(value)
|
||||||
|
return FALSE
|
||||||
|
if selector.operator == "$=":
|
||||||
|
return (
|
||||||
|
(lambda el: attribute_value(el).endswith(value)) if value else FALSE
|
||||||
|
)
|
||||||
|
if selector.operator == "*=":
|
||||||
|
return (lambda el: value in attribute_value(el)) if value else FALSE
|
||||||
|
raise SelectorError("Unknown attribute operator", selector.operator)
|
||||||
|
# In any namespace
|
||||||
|
raise NotImplementedError # TODO
|
||||||
|
|
||||||
|
def _compile_pseudoclass(self, selector: parser.PseudoClassSelector):
|
||||||
|
if selector.name in ("link", "any-link", "local-link"):
|
||||||
|
|
||||||
|
def ancestors_or_self(el):
|
||||||
|
yield el
|
||||||
|
yield from iterancestors(el)
|
||||||
|
|
||||||
|
return lambda el: any(
|
||||||
|
e.TAG == "a" and super(etree.ElementBase, e).get("href", "") != "" # type: ignore
|
||||||
|
for e in ancestors_or_self(el)
|
||||||
|
)
|
||||||
|
if selector.name in (
|
||||||
|
"visited",
|
||||||
|
"hover",
|
||||||
|
"active",
|
||||||
|
"focus",
|
||||||
|
"focus-within",
|
||||||
|
"focus-visible",
|
||||||
|
"target",
|
||||||
|
"target-within",
|
||||||
|
"current",
|
||||||
|
"past",
|
||||||
|
"future",
|
||||||
|
"playing",
|
||||||
|
"paused",
|
||||||
|
"seeking",
|
||||||
|
"buffering",
|
||||||
|
"stalled",
|
||||||
|
"muted",
|
||||||
|
"volume-locked",
|
||||||
|
"user-valid",
|
||||||
|
"user-invalid",
|
||||||
|
):
|
||||||
|
# Not applicable in a static context: never match.
|
||||||
|
return FALSE
|
||||||
|
if selector.name in ("enabled", "disabled", "checked"):
|
||||||
|
# Not applicable to SVG
|
||||||
|
return FALSE
|
||||||
|
if selector.name in ("root", "scope"):
|
||||||
|
return lambda el: el.getparent() is None
|
||||||
|
if selector.name == "first-child":
|
||||||
|
return lambda el: getprevious(el) is None
|
||||||
|
if selector.name == "last-child":
|
||||||
|
return lambda el: getnext(el) is None
|
||||||
|
if selector.name == "first-of-type":
|
||||||
|
return lambda el: all(
|
||||||
|
s.tag != el.tag for s in itersiblings(el, preceding=True)
|
||||||
|
)
|
||||||
|
if selector.name == "last-of-type":
|
||||||
|
return lambda el: all(s.tag != el.tag for s in itersiblings(el))
|
||||||
|
if selector.name == "only-child":
|
||||||
|
return lambda el: getnext(el) is None and getprevious(el) is None
|
||||||
|
if selector.name == "only-of-type":
|
||||||
|
return lambda el: all(s.tag != el.tag for s in itersiblings(el)) and all(
|
||||||
|
s.tag != el.tag for s in itersiblings(el, preceding=True)
|
||||||
|
)
|
||||||
|
if selector.name == "empty":
|
||||||
|
return lambda el: not list(el) and el.text is None
|
||||||
|
raise SelectorError("Unknown pseudo-class", selector.name)
|
||||||
|
|
||||||
|
def _compile_lang(self, selector: parser.FunctionalPseudoClassSelector):
|
||||||
|
langs = []
|
||||||
|
tokens = [
|
||||||
|
token
|
||||||
|
for token in selector.arguments
|
||||||
|
if token.type not in ("whitespace", "comment")
|
||||||
|
]
|
||||||
|
while tokens:
|
||||||
|
token = tokens.pop(0)
|
||||||
|
if token.type == "ident":
|
||||||
|
langs.append(token.lower_value)
|
||||||
|
elif token.type == "string":
|
||||||
|
langs.append(ascii_lower(token.value))
|
||||||
|
else:
|
||||||
|
raise SelectorError("Invalid arguments for :lang()")
|
||||||
|
if tokens:
|
||||||
|
token = tokens.pop(0)
|
||||||
|
if token.type != "ident" and token.value != ",":
|
||||||
|
raise SelectorError("Invalid arguments for :lang()")
|
||||||
|
|
||||||
|
def haslang(el, lang):
|
||||||
|
print(
|
||||||
|
el.get("lang"),
|
||||||
|
lang,
|
||||||
|
el.get("lang", "") == lang or el.get("lang", "").startswith(lang + "-"),
|
||||||
|
)
|
||||||
|
return el.get("lang", "").lower() == lang or el.get(
|
||||||
|
"lang", ""
|
||||||
|
).lower().startswith(lang + "-")
|
||||||
|
|
||||||
|
return lambda el: any(
|
||||||
|
haslang(el, lang) or any(haslang(el2, lang) for el2 in iterancestors(el))
|
||||||
|
for lang in langs
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compile_functional_pseudoclass(
|
||||||
|
self, selector: parser.FunctionalPseudoClassSelector
|
||||||
|
):
|
||||||
|
if selector.name == "lang":
|
||||||
|
return self._compile_lang(selector)
|
||||||
|
nth: List[str] = []
|
||||||
|
selector_list: List[str] = []
|
||||||
|
current_list = nth
|
||||||
|
for argument in selector.arguments:
|
||||||
|
if argument.type == "ident" and argument.value == "of":
|
||||||
|
if current_list is nth:
|
||||||
|
current_list = selector_list
|
||||||
|
continue
|
||||||
|
current_list.append(argument)
|
||||||
|
|
||||||
|
if selector_list:
|
||||||
|
compiled = tuple(
|
||||||
|
self.compile_node(selector.parsed_tree)
|
||||||
|
for selector in parser.parse(selector_list)
|
||||||
|
)
|
||||||
|
test = lambda el: all(expr(el) for expr in compiled)
|
||||||
|
|
||||||
|
else:
|
||||||
|
test = TRUE
|
||||||
|
|
||||||
|
if selector.name == "nth-child":
|
||||||
|
count = lambda el: sum(
|
||||||
|
1 for e in itersiblings(el, preceding=True) if test(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
elif selector.name == "nth-last-child":
|
||||||
|
count = lambda el: sum(1 for e in itersiblings(el) if test(e))
|
||||||
|
elif selector.name == "nth-of-type":
|
||||||
|
count = lambda el: sum(
|
||||||
|
1
|
||||||
|
for s in (e for e in itersiblings(el, preceding=True) if test(e))
|
||||||
|
if s.tag == el.tag
|
||||||
|
)
|
||||||
|
|
||||||
|
elif selector.name == "nth-last-of-type":
|
||||||
|
count = lambda el: sum(
|
||||||
|
1 for s in (e for e in itersiblings(el) if test(e)) if s.tag == el.tag
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise SelectorError("Unknown pseudo-class", selector.name)
|
||||||
|
|
||||||
|
count_func = lambda el: count(el) if test(el) else float("nan")
|
||||||
|
|
||||||
|
result = parse_nth(nth)
|
||||||
|
if result is None:
|
||||||
|
raise SelectorError(f"Invalid arguments for :{selector.name}()")
|
||||||
|
a, b = result
|
||||||
|
# x is the number of siblings before/after the element
|
||||||
|
# Matches if a positive or zero integer n exists so that:
|
||||||
|
# x = a*n + b-1
|
||||||
|
# x = a*n + B
|
||||||
|
B = b - 1
|
||||||
|
if a == 0:
|
||||||
|
# x = B
|
||||||
|
return lambda el: count_func(el) == B
|
||||||
|
|
||||||
|
# n = (x - B) / a
|
||||||
|
def evaluator(el):
|
||||||
|
n, r = divmod(count_func(el) - B, a)
|
||||||
|
return r == 0 and n >= 0
|
||||||
|
|
||||||
|
return evaluator
|
||||||
|
|
||||||
|
def compile_node(self, selector):
|
||||||
|
"""Return a boolean expression, as a callable.
|
||||||
|
|
||||||
|
When evaluated in a context where the `el` variable is an
|
||||||
|
:class:`cssselect2.tree.Element` object, tells whether the element is a
|
||||||
|
subject of `selector`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self._func_map[selector.__class__](selector)
|
||||||
|
except KeyError as e:
|
||||||
|
raise TypeError(type(selector), selector) from e
|
||||||
|
|
||||||
|
|
||||||
|
CSSCompiler = BooleanCompiler()
|
||||||
548
deps/inkex/css/parser.py
vendored
Normal file
548
deps/inkex/css/parser.py
vendored
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
# Forked from cssselect2, 1.2.1, BSD License
|
||||||
|
|
||||||
|
"""Parse CSS declarations."""
|
||||||
|
|
||||||
|
from tinycss2 import parse_component_value_list
|
||||||
|
|
||||||
|
__all__ = ["parse"]
|
||||||
|
|
||||||
|
SUPPORTED_PSEUDO_ELEMENTS = {
|
||||||
|
# As per CSS Pseudo-Elements Module Level 4
|
||||||
|
"first-line",
|
||||||
|
"first-letter",
|
||||||
|
"prefix",
|
||||||
|
"postfix",
|
||||||
|
"selection",
|
||||||
|
"target-text",
|
||||||
|
"spelling-error",
|
||||||
|
"grammar-error",
|
||||||
|
"before",
|
||||||
|
"after",
|
||||||
|
"marker",
|
||||||
|
"placeholder",
|
||||||
|
"file-selector-button",
|
||||||
|
# As per CSS Generated Content for Paged Media Module
|
||||||
|
"footnote-call",
|
||||||
|
"footnote-marker",
|
||||||
|
# As per CSS Scoping Module Level 1
|
||||||
|
"content",
|
||||||
|
"shadow",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse(input, namespaces=None, forgiving=False, relative=False):
|
||||||
|
"""Yield tinycss2 selectors found in given ``input``.
|
||||||
|
|
||||||
|
:param input:
|
||||||
|
A string, or an iterable of tinycss2 component values.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(input, str):
|
||||||
|
input = parse_component_value_list(input)
|
||||||
|
tokens = TokenStream(input)
|
||||||
|
namespaces = namespaces or {}
|
||||||
|
try:
|
||||||
|
yield parse_selector(tokens, namespaces, relative)
|
||||||
|
except SelectorError as exception:
|
||||||
|
if forgiving:
|
||||||
|
return
|
||||||
|
raise exception
|
||||||
|
while 1:
|
||||||
|
next = tokens.next()
|
||||||
|
if next is None:
|
||||||
|
return
|
||||||
|
elif next == ",":
|
||||||
|
try:
|
||||||
|
yield parse_selector(tokens, namespaces, relative)
|
||||||
|
except SelectorError as exception:
|
||||||
|
if not forgiving:
|
||||||
|
raise exception
|
||||||
|
else:
|
||||||
|
if not forgiving:
|
||||||
|
raise SelectorError(next, f"unexpected {next.type} token.")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_selector(tokens, namespaces, relative=False):
|
||||||
|
tokens.skip_whitespace_and_comment()
|
||||||
|
if relative:
|
||||||
|
peek = tokens.peek()
|
||||||
|
if peek in (">", "+", "~"):
|
||||||
|
initial_combinator = peek.value
|
||||||
|
tokens.next()
|
||||||
|
else:
|
||||||
|
initial_combinator = " "
|
||||||
|
tokens.skip_whitespace_and_comment()
|
||||||
|
result, pseudo_element = parse_compound_selector(tokens, namespaces)
|
||||||
|
while 1:
|
||||||
|
has_whitespace = tokens.skip_whitespace()
|
||||||
|
while tokens.skip_comment():
|
||||||
|
has_whitespace = tokens.skip_whitespace() or has_whitespace
|
||||||
|
selector = Selector(result, pseudo_element)
|
||||||
|
if relative:
|
||||||
|
selector = RelativeSelector(initial_combinator, selector)
|
||||||
|
if pseudo_element is not None:
|
||||||
|
return selector
|
||||||
|
peek = tokens.peek()
|
||||||
|
if peek is None or peek == ",":
|
||||||
|
return selector
|
||||||
|
elif peek in (">", "+", "~"):
|
||||||
|
combinator = peek.value
|
||||||
|
tokens.next()
|
||||||
|
elif has_whitespace:
|
||||||
|
combinator = " "
|
||||||
|
else:
|
||||||
|
return selector
|
||||||
|
compound, pseudo_element = parse_compound_selector(tokens, namespaces)
|
||||||
|
result = CombinedSelector(result, combinator, compound)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_compound_selector(tokens, namespaces):
|
||||||
|
type_selectors = parse_type_selector(tokens, namespaces)
|
||||||
|
simple_selectors = type_selectors if type_selectors is not None else []
|
||||||
|
while 1:
|
||||||
|
simple_selector, pseudo_element = parse_simple_selector(tokens, namespaces)
|
||||||
|
if pseudo_element is not None or simple_selector is None:
|
||||||
|
break
|
||||||
|
simple_selectors.append(simple_selector)
|
||||||
|
|
||||||
|
if simple_selectors or (type_selectors, pseudo_element) != (None, None):
|
||||||
|
return CompoundSelector(simple_selectors), pseudo_element
|
||||||
|
|
||||||
|
peek = tokens.peek()
|
||||||
|
peek_type = peek.type if peek else "EOF"
|
||||||
|
raise SelectorError(peek, f"expected a compound selector, got {peek_type}")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_type_selector(tokens, namespaces):
|
||||||
|
tokens.skip_whitespace()
|
||||||
|
qualified_name = parse_qualified_name(tokens, namespaces)
|
||||||
|
if qualified_name is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
simple_selectors = []
|
||||||
|
namespace, local_name = qualified_name
|
||||||
|
if local_name is not None:
|
||||||
|
simple_selectors.append(LocalNameSelector(local_name))
|
||||||
|
if namespace is not None:
|
||||||
|
simple_selectors.append(NamespaceSelector(namespace))
|
||||||
|
return simple_selectors
|
||||||
|
|
||||||
|
|
||||||
|
def parse_simple_selector(tokens, namespaces):
|
||||||
|
peek = tokens.peek()
|
||||||
|
if peek is None:
|
||||||
|
return None, None
|
||||||
|
if peek.type == "hash" and peek.is_identifier:
|
||||||
|
tokens.next()
|
||||||
|
return IDSelector(peek.value), None
|
||||||
|
elif peek == ".":
|
||||||
|
tokens.next()
|
||||||
|
next = tokens.next()
|
||||||
|
if next is None or next.type != "ident":
|
||||||
|
raise SelectorError(next, f"Expected a class name, got {next}")
|
||||||
|
return ClassSelector(next.value), None
|
||||||
|
elif peek.type == "[] block":
|
||||||
|
tokens.next()
|
||||||
|
attr = parse_attribute_selector(TokenStream(peek.content), namespaces)
|
||||||
|
return attr, None
|
||||||
|
elif peek == ":":
|
||||||
|
tokens.next()
|
||||||
|
next = tokens.next()
|
||||||
|
if next == ":":
|
||||||
|
next = tokens.next()
|
||||||
|
if next is None or next.type != "ident":
|
||||||
|
raise SelectorError(next, f"Expected a pseudo-element name, got {next}")
|
||||||
|
value = next.lower_value
|
||||||
|
if value not in SUPPORTED_PSEUDO_ELEMENTS:
|
||||||
|
raise SelectorError(
|
||||||
|
next, f"Expected a supported pseudo-element, got {value}"
|
||||||
|
)
|
||||||
|
return None, value
|
||||||
|
elif next is not None and next.type == "ident":
|
||||||
|
name = next.lower_value
|
||||||
|
if name in ("before", "after", "first-line", "first-letter"):
|
||||||
|
return None, name
|
||||||
|
else:
|
||||||
|
return PseudoClassSelector(name), None
|
||||||
|
elif next is not None and next.type == "function":
|
||||||
|
name = next.lower_name
|
||||||
|
if name in ("is", "where", "not", "has"):
|
||||||
|
return parse_logical_combination(next, namespaces, name), None
|
||||||
|
else:
|
||||||
|
return (FunctionalPseudoClassSelector(name, next.arguments), None)
|
||||||
|
else:
|
||||||
|
raise SelectorError(next, f"unexpected {next} token.")
|
||||||
|
else:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_logical_combination(matches_any_token, namespaces, name):
|
||||||
|
forgiving = True
|
||||||
|
relative = False
|
||||||
|
if name == "is":
|
||||||
|
selector_class = MatchesAnySelector
|
||||||
|
elif name == "where":
|
||||||
|
selector_class = SpecificityAdjustmentSelector
|
||||||
|
elif name == "not":
|
||||||
|
forgiving = False
|
||||||
|
selector_class = NegationSelector
|
||||||
|
elif name == "has":
|
||||||
|
relative = True
|
||||||
|
selector_class = RelationalSelector
|
||||||
|
|
||||||
|
selectors = [
|
||||||
|
selector
|
||||||
|
for selector in parse(
|
||||||
|
matches_any_token.arguments, namespaces, forgiving, relative
|
||||||
|
)
|
||||||
|
if selector.pseudo_element is None
|
||||||
|
]
|
||||||
|
return selector_class(selectors)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_attribute_selector(tokens, namespaces):
|
||||||
|
tokens.skip_whitespace()
|
||||||
|
qualified_name = parse_qualified_name(tokens, namespaces, is_attribute=True)
|
||||||
|
if qualified_name is None:
|
||||||
|
next = tokens.next()
|
||||||
|
raise SelectorError(next, f"expected attribute name, got {next}")
|
||||||
|
namespace, local_name = qualified_name
|
||||||
|
|
||||||
|
tokens.skip_whitespace()
|
||||||
|
peek = tokens.peek()
|
||||||
|
if peek is None:
|
||||||
|
operator = None
|
||||||
|
value = None
|
||||||
|
elif peek in ("=", "~=", "|=", "^=", "$=", "*="):
|
||||||
|
operator = peek.value
|
||||||
|
tokens.next()
|
||||||
|
tokens.skip_whitespace()
|
||||||
|
next = tokens.next()
|
||||||
|
if next is None or next.type not in ("ident", "string"):
|
||||||
|
next_type = "None" if next is None else next.type
|
||||||
|
raise SelectorError(next, f"expected attribute value, got {next_type}")
|
||||||
|
value = next.value
|
||||||
|
else:
|
||||||
|
raise SelectorError(peek, f"expected attribute selector operator, got {peek}")
|
||||||
|
|
||||||
|
tokens.skip_whitespace()
|
||||||
|
next = tokens.next()
|
||||||
|
case_sensitive = None
|
||||||
|
if next is not None:
|
||||||
|
if next.type == "ident" and next.value.lower() == "i":
|
||||||
|
case_sensitive = False
|
||||||
|
elif next.type == "ident" and next.value.lower() == "s":
|
||||||
|
case_sensitive = True
|
||||||
|
else:
|
||||||
|
raise SelectorError(next, f"expected ], got {next.type}")
|
||||||
|
return AttributeSelector(namespace, local_name, operator, value, case_sensitive)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_qualified_name(tokens, namespaces, is_attribute=False):
|
||||||
|
"""Return ``(namespace, local)`` for given tokens.
|
||||||
|
|
||||||
|
Can also return ``None`` for a wildcard.
|
||||||
|
|
||||||
|
The empty string for ``namespace`` means "no namespace".
|
||||||
|
|
||||||
|
"""
|
||||||
|
peek = tokens.peek()
|
||||||
|
if peek is None:
|
||||||
|
return None
|
||||||
|
if peek.type == "ident":
|
||||||
|
first_ident = tokens.next()
|
||||||
|
peek = tokens.peek()
|
||||||
|
if peek != "|":
|
||||||
|
namespace = "" if is_attribute else namespaces.get(None, None)
|
||||||
|
return namespace, (first_ident.value, first_ident.lower_value)
|
||||||
|
tokens.next()
|
||||||
|
namespace = namespaces.get(first_ident.value)
|
||||||
|
if namespace is None:
|
||||||
|
raise SelectorError(
|
||||||
|
first_ident, f"undefined namespace prefix: {first_ident.value}"
|
||||||
|
)
|
||||||
|
elif peek == "*":
|
||||||
|
next = tokens.next()
|
||||||
|
peek = tokens.peek()
|
||||||
|
if peek != "|":
|
||||||
|
if is_attribute:
|
||||||
|
raise SelectorError(next, f"expected local name, got {next.type}")
|
||||||
|
return namespaces.get(None, None), None
|
||||||
|
tokens.next()
|
||||||
|
namespace = None
|
||||||
|
elif peek == "|":
|
||||||
|
tokens.next()
|
||||||
|
namespace = ""
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# If we get here, we just consumed '|' and set ``namespace``
|
||||||
|
next = tokens.next()
|
||||||
|
if next.type == "ident":
|
||||||
|
return namespace, (next.value, next.lower_value)
|
||||||
|
elif next == "*" and not is_attribute:
|
||||||
|
return namespace, None
|
||||||
|
else:
|
||||||
|
raise SelectorError(next, f"expected local name, got {next.type}")
|
||||||
|
|
||||||
|
|
||||||
|
class SelectorError(ValueError):
|
||||||
|
"""A specialized ``ValueError`` for invalid selectors."""
|
||||||
|
|
||||||
|
|
||||||
|
class TokenStream:
|
||||||
|
def __init__(self, tokens):
|
||||||
|
self.tokens = iter(tokens)
|
||||||
|
self.peeked = [] # In reversed order
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
if self.peeked:
|
||||||
|
return self.peeked.pop()
|
||||||
|
else:
|
||||||
|
return next(self.tokens, None)
|
||||||
|
|
||||||
|
def peek(self):
|
||||||
|
if not self.peeked:
|
||||||
|
self.peeked.append(next(self.tokens, None))
|
||||||
|
return self.peeked[-1]
|
||||||
|
|
||||||
|
def skip(self, skip_types):
|
||||||
|
found = False
|
||||||
|
while 1:
|
||||||
|
peek = self.peek()
|
||||||
|
if peek is None or peek.type not in skip_types:
|
||||||
|
break
|
||||||
|
self.next()
|
||||||
|
found = True
|
||||||
|
return found
|
||||||
|
|
||||||
|
def skip_whitespace(self):
|
||||||
|
return self.skip(["whitespace"])
|
||||||
|
|
||||||
|
def skip_comment(self):
|
||||||
|
return self.skip(["comment"])
|
||||||
|
|
||||||
|
def skip_whitespace_and_comment(self):
|
||||||
|
return self.skip(["comment", "whitespace"])
|
||||||
|
|
||||||
|
|
||||||
|
class Selector:
|
||||||
|
def __init__(self, tree, pseudo_element=None):
|
||||||
|
self.parsed_tree = tree
|
||||||
|
self.pseudo_element = pseudo_element
|
||||||
|
if pseudo_element is None:
|
||||||
|
#: Tuple of 3 integers: http://www.w3.org/TR/selectors/#specificity
|
||||||
|
self.specificity = tree.specificity
|
||||||
|
else:
|
||||||
|
a, b, c = tree.specificity
|
||||||
|
self.specificity = a, b, c + 1
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
pseudo = f"::{self.pseudo_element}" if self.pseudo_element else ""
|
||||||
|
return f"{self.parsed_tree!r}{pseudo}"
|
||||||
|
|
||||||
|
|
||||||
|
class RelativeSelector:
|
||||||
|
def __init__(self, combinator, selector):
|
||||||
|
self.combinator = combinator
|
||||||
|
self.selector = selector
|
||||||
|
|
||||||
|
@property
|
||||||
|
def specificity(self):
|
||||||
|
return self.selector.specificity
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pseudo_element(self):
|
||||||
|
return self.selector.pseudo_element
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"{self.selector!r}"
|
||||||
|
if self.combinator == " "
|
||||||
|
else f"{self.combinator} {self.selector!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CombinedSelector:
|
||||||
|
def __init__(self, left, combinator, right):
|
||||||
|
#: Combined or compound selector
|
||||||
|
self.left = left
|
||||||
|
# One of `` `` (a single space), ``>``, ``+`` or ``~``.
|
||||||
|
self.combinator = combinator
|
||||||
|
#: compound selector
|
||||||
|
self.right = right
|
||||||
|
|
||||||
|
@property
|
||||||
|
def specificity(self):
|
||||||
|
a1, b1, c1 = self.left.specificity
|
||||||
|
a2, b2, c2 = self.right.specificity
|
||||||
|
return a1 + a2, b1 + b2, c1 + c2
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.left!r}{self.combinator}{self.right!r}"
|
||||||
|
|
||||||
|
|
||||||
|
class CompoundSelector:
|
||||||
|
def __init__(self, simple_selectors):
|
||||||
|
self.simple_selectors = simple_selectors
|
||||||
|
|
||||||
|
@property
|
||||||
|
def specificity(self):
|
||||||
|
if self.simple_selectors:
|
||||||
|
# zip(*foo) turns [(a1, b1, c1), (a2, b2, c2), ...]
|
||||||
|
# into [(a1, a2, ...), (b1, b2, ...), (c1, c2, ...)]
|
||||||
|
return tuple(
|
||||||
|
map(sum, zip(*(sel.specificity for sel in self.simple_selectors)))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return 0, 0, 0
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "".join(map(repr, self.simple_selectors))
|
||||||
|
|
||||||
|
|
||||||
|
class LocalNameSelector:
|
||||||
|
specificity = 0, 0, 1
|
||||||
|
|
||||||
|
def __init__(self, local_name):
|
||||||
|
self.local_name, self.lower_local_name = local_name
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.local_name
|
||||||
|
|
||||||
|
|
||||||
|
class NamespaceSelector:
|
||||||
|
specificity = 0, 0, 0
|
||||||
|
|
||||||
|
def __init__(self, namespace):
|
||||||
|
#: The namespace URL as a string,
|
||||||
|
#: or the empty string for elements not in any namespace.
|
||||||
|
self.namespace = namespace
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self.namespace == "":
|
||||||
|
return "|"
|
||||||
|
else:
|
||||||
|
return f"{{{self.namespace}}}|"
|
||||||
|
|
||||||
|
|
||||||
|
class IDSelector:
|
||||||
|
specificity = 1, 0, 0
|
||||||
|
|
||||||
|
def __init__(self, ident):
|
||||||
|
self.ident = ident
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"#{self.ident}"
|
||||||
|
|
||||||
|
|
||||||
|
class ClassSelector:
|
||||||
|
specificity = 0, 1, 0
|
||||||
|
|
||||||
|
def __init__(self, class_name):
|
||||||
|
self.class_name = class_name
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f".{self.class_name}"
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeSelector:
|
||||||
|
specificity = 0, 1, 0
|
||||||
|
|
||||||
|
def __init__(self, namespace, name, operator, value, case_sensitive):
|
||||||
|
self.namespace = namespace
|
||||||
|
self.name, self.lower_name = name
|
||||||
|
#: A string like ``=`` or ``~=``, or None for ``[attr]`` selectors
|
||||||
|
self.operator = operator
|
||||||
|
#: A string, or None for ``[attr]`` selectors
|
||||||
|
self.value = value
|
||||||
|
#: ``True`` if case-sensitive, ``False`` if case-insensitive, ``None``
|
||||||
|
#: if depends on the document language
|
||||||
|
self.case_sensitive = case_sensitive
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
namespace = "*|" if self.namespace is None else f"{{{self.namespace}}}"
|
||||||
|
case_sensitive = (
|
||||||
|
""
|
||||||
|
if self.case_sensitive is None
|
||||||
|
else f" {'s' if self.case_sensitive else 'i'}"
|
||||||
|
)
|
||||||
|
return f"[{namespace}{self.name}{self.operator}{self.value!r}{case_sensitive}]"
|
||||||
|
|
||||||
|
|
||||||
|
class PseudoClassSelector:
|
||||||
|
specificity = 0, 1, 0
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return ":" + self.name
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionalPseudoClassSelector:
|
||||||
|
specificity = 0, 1, 0
|
||||||
|
|
||||||
|
def __init__(self, name, arguments):
|
||||||
|
self.name = name
|
||||||
|
self.arguments = arguments
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f":{self.name}{tuple(self.arguments)!r}"
|
||||||
|
|
||||||
|
|
||||||
|
class NegationSelector:
|
||||||
|
def __init__(self, selector_list):
|
||||||
|
self.selector_list = selector_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def specificity(self):
|
||||||
|
if self.selector_list:
|
||||||
|
return max(selector.specificity for selector in self.selector_list)
|
||||||
|
else:
|
||||||
|
return (0, 0, 0)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f":not({', '.join(repr(sel) for sel in self.selector_list)})"
|
||||||
|
|
||||||
|
|
||||||
|
class RelationalSelector:
|
||||||
|
def __init__(self, selector_list):
|
||||||
|
self.selector_list = selector_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def specificity(self):
|
||||||
|
if self.selector_list:
|
||||||
|
return max(selector.specificity for selector in self.selector_list)
|
||||||
|
else:
|
||||||
|
return (0, 0, 0)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f":has({', '.join(repr(sel) for sel in self.selector_list)})"
|
||||||
|
|
||||||
|
|
||||||
|
class MatchesAnySelector:
|
||||||
|
def __init__(self, selector_list):
|
||||||
|
self.selector_list = selector_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def specificity(self):
|
||||||
|
if self.selector_list:
|
||||||
|
return max(selector.specificity for selector in self.selector_list)
|
||||||
|
else:
|
||||||
|
return (0, 0, 0)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f":is({', '.join(repr(sel) for sel in self.selector_list)})"
|
||||||
|
|
||||||
|
|
||||||
|
class SpecificityAdjustmentSelector:
|
||||||
|
def __init__(self, selector_list):
|
||||||
|
self.selector_list = selector_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def specificity(self):
|
||||||
|
return (0, 0, 0)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f":where({', '.join(repr(sel) for sel in self.selector_list)})"
|
||||||
4
deps/inkex/deprecated-simple/README.rst
vendored
Normal file
4
deps/inkex/deprecated-simple/README.rst
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# coding=utf-8This directory contains compatibility layers for all the `simple` modules, such as `simplepath` and `simplestyle`
|
||||||
|
|
||||||
|
This directory IS NOT a module path, to denote this we are using a dash in the name and there is no '__init__.py'
|
||||||
|
|
||||||
46
deps/inkex/deprecated-simple/bezmisc.py
vendored
Normal file
46
deps/inkex/deprecated-simple/bezmisc.py
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# pylint: disable=invalid-name,unused-argument
|
||||||
|
"""Deprecated bezmisc API"""
|
||||||
|
|
||||||
|
from inkex.deprecated import deprecate
|
||||||
|
from inkex import bezier
|
||||||
|
|
||||||
|
bezierparameterize = deprecate(bezier.bezierparameterize)
|
||||||
|
linebezierintersect = deprecate(bezier.linebezierintersect)
|
||||||
|
bezierpointatt = deprecate(bezier.bezierpointatt)
|
||||||
|
bezierslopeatt = deprecate(bezier.bezierslopeatt)
|
||||||
|
beziertatslope = deprecate(bezier.beziertatslope)
|
||||||
|
tpoint = deprecate(bezier.tpoint)
|
||||||
|
beziersplitatt = deprecate(bezier.beziersplitatt)
|
||||||
|
pointdistance = deprecate(bezier.pointdistance)
|
||||||
|
Gravesen_addifclose = deprecate(bezier.addifclose)
|
||||||
|
balf = deprecate(bezier.balf)
|
||||||
|
bezierlengthSimpson = deprecate(bezier.bezierlength)
|
||||||
|
beziertatlength = deprecate(bezier.beziertatlength)
|
||||||
|
bezierlength = bezierlengthSimpson
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def Simpson(func, a, b, n_limit, tolerance):
|
||||||
|
"""bezier.simpson(a, b, n_limit, tolerance, balf_arguments)"""
|
||||||
|
raise AttributeError(
|
||||||
|
"""Because bezmisc.Simpson used global variables, it's not possible to
|
||||||
|
call the replacement code automatically. In fact it's unlikely you were
|
||||||
|
using the code or functionality you think you were since it's a highly
|
||||||
|
broken way of writing python."""
|
||||||
|
)
|
||||||
25
deps/inkex/deprecated-simple/cspsubdiv.py
vendored
Normal file
25
deps/inkex/deprecated-simple/cspsubdiv.py
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
"""Deprecated cspsubdiv API"""
|
||||||
|
|
||||||
|
from inkex.deprecated import deprecate
|
||||||
|
from inkex import bezier
|
||||||
|
|
||||||
|
maxdist = deprecate(bezier.maxdist)
|
||||||
|
cspsubdiv = deprecate(bezier.cspsubdiv)
|
||||||
|
subdiv = deprecate(bezier.subdiv)
|
||||||
52
deps/inkex/deprecated-simple/cubicsuperpath.py
vendored
Normal file
52
deps/inkex/deprecated-simple/cubicsuperpath.py
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
"""Deprecated cubic super path API"""
|
||||||
|
|
||||||
|
from inkex.deprecated import deprecate
|
||||||
|
from inkex import paths
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def ArcToPath(p1, params):
|
||||||
|
return paths.arc_to_path(p1, params)
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def CubicSuperPath(simplepath):
|
||||||
|
return paths.Path(simplepath).to_superpath()
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def unCubicSuperPath(csp):
|
||||||
|
return paths.CubicSuperPath(csp).to_path().to_arrays()
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def parsePath(d):
|
||||||
|
return paths.CubicSuperPath(paths.Path(d))
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def formatPath(p):
|
||||||
|
return str(paths.Path(unCubicSuperPath(p)))
|
||||||
|
|
||||||
|
|
||||||
|
matprod = deprecate(paths.matprod)
|
||||||
|
rotmat = deprecate(paths.rotmat)
|
||||||
|
applymat = deprecate(paths.applymat)
|
||||||
|
norm = deprecate(paths.norm)
|
||||||
92
deps/inkex/deprecated-simple/ffgeom.py
vendored
Normal file
92
deps/inkex/deprecated-simple/ffgeom.py
vendored
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# pylint: disable=invalid-name,missing-docstring
|
||||||
|
"""Deprecated ffgeom API"""
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from inkex.deprecated import deprecate
|
||||||
|
from inkex.transforms import DirectedLineSegment as NewSeg
|
||||||
|
|
||||||
|
try:
|
||||||
|
NaN = float("NaN")
|
||||||
|
except ValueError:
|
||||||
|
PosInf = 1e300000
|
||||||
|
NaN = PosInf / PosInf
|
||||||
|
|
||||||
|
|
||||||
|
class Point(namedtuple("Point", "x y")):
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if isinstance(key, str):
|
||||||
|
key = "xy".index(key)
|
||||||
|
return super(Point, self).__getitem__(key)
|
||||||
|
|
||||||
|
|
||||||
|
class Segment(NewSeg):
|
||||||
|
@deprecate
|
||||||
|
def __init__(self, e0, e1):
|
||||||
|
"""inkex.transforms.DirectedLineSegment((x1, y1), (x2, y2))"""
|
||||||
|
if isinstance(e0, dict):
|
||||||
|
e0 = (e0["x"], e0["y"])
|
||||||
|
if isinstance(e1, dict):
|
||||||
|
e1 = (e1["x"], e1["y"])
|
||||||
|
super(Segment, self).__init__(e0, e1)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if key:
|
||||||
|
return {"x": self.x.maximum, "y": self.y.maximum}
|
||||||
|
return {"x": self.x.minimum, "y": self.y.minimum}
|
||||||
|
|
||||||
|
delta_x = lambda self: self.width
|
||||||
|
delta_y = lambda self: self.height
|
||||||
|
run = delta_x
|
||||||
|
rise = delta_y
|
||||||
|
|
||||||
|
def distanceToPoint(self, p):
|
||||||
|
return self.distance_to_point(p["x"], p["y"])
|
||||||
|
|
||||||
|
def perpDistanceToPoint(self, p):
|
||||||
|
return self.perp_distance(p["x"], p["y"])
|
||||||
|
|
||||||
|
def angle(self):
|
||||||
|
return super(Segment, self).angle
|
||||||
|
|
||||||
|
def length(self):
|
||||||
|
return super(Segment, self).length
|
||||||
|
|
||||||
|
def pointAtLength(self, length):
|
||||||
|
return self.point_at_length(length)
|
||||||
|
|
||||||
|
def pointAtRatio(self, ratio):
|
||||||
|
return self.point_at_ratio(ratio)
|
||||||
|
|
||||||
|
def createParallel(self, p):
|
||||||
|
self.parallel(p["x"], p["y"])
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def intersectSegments(s1, s2):
|
||||||
|
"""transforms.Segment(s1).intersect(s2)"""
|
||||||
|
return Point(*s1.intersect(s2))
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def dot(s1, s2):
|
||||||
|
"""transforms.Segment(s1).dot(s2)"""
|
||||||
|
return s1.dot(s2)
|
||||||
80
deps/inkex/deprecated-simple/run_command.py
vendored
Normal file
80
deps/inkex/deprecated-simple/run_command.py
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2008 Stephen Silver
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Deprecated module for running SVG-generating commands in Inkscape extensions
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
|
|
||||||
|
from inkex.deprecated import deprecate
|
||||||
|
|
||||||
|
|
||||||
|
def run(command_format, prog_name):
|
||||||
|
"""inkex.commands.call(...)"""
|
||||||
|
svgfile = tempfile.mktemp(".svg")
|
||||||
|
command = command_format % svgfile
|
||||||
|
msg = None
|
||||||
|
# ps2pdf may attempt to write to the current directory, which may not
|
||||||
|
# be writeable, so we switch to the temp directory first.
|
||||||
|
try:
|
||||||
|
os.chdir(tempfile.gettempdir())
|
||||||
|
except IOError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = Popen(command, shell=True, stdout=PIPE, stderr=PIPE)
|
||||||
|
return_code = proc.wait()
|
||||||
|
out = proc.stdout.read()
|
||||||
|
err = proc.stderr.read()
|
||||||
|
|
||||||
|
if msg is None:
|
||||||
|
if return_code:
|
||||||
|
msg = "{} failed:\n{}\n{}\n".format(prog_name, out, err)
|
||||||
|
elif err:
|
||||||
|
sys.stderr.write(
|
||||||
|
"{} executed but logged the following error:\n{}\n{}\n".format(
|
||||||
|
prog_name, out, err
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as inst:
|
||||||
|
msg = "Error attempting to run {}: {}".format(prog_name, str(inst))
|
||||||
|
|
||||||
|
# If successful, copy the output file to stdout.
|
||||||
|
if msg is None:
|
||||||
|
if os.name == "nt": # make stdout work in binary on Windows
|
||||||
|
import msvcrt
|
||||||
|
|
||||||
|
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
||||||
|
try:
|
||||||
|
with open(svgfile, "rb") as fhl:
|
||||||
|
sys.stdout.write(fhl.read().decode(sys.stdout.encoding))
|
||||||
|
except IOError as inst:
|
||||||
|
msg = "Error reading temporary file: {}".format(str(inst))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Clean up.
|
||||||
|
os.remove(svgfile)
|
||||||
|
except (IOError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Output error message (if any) and exit.
|
||||||
|
return msg
|
||||||
68
deps/inkex/deprecated-simple/simplepath.py
vendored
Normal file
68
deps/inkex/deprecated-simple/simplepath.py
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
# COPYRIGHT
|
||||||
|
#
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Depreicated simplepath replacements with documentation
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
from inkex.deprecated import deprecate, DeprecatedDict
|
||||||
|
from inkex.transforms import Transform
|
||||||
|
from inkex.paths import Path
|
||||||
|
|
||||||
|
pathdefs = DeprecatedDict(
|
||||||
|
{
|
||||||
|
"M": ["L", 2, [float, float], ["x", "y"]],
|
||||||
|
"L": ["L", 2, [float, float], ["x", "y"]],
|
||||||
|
"H": ["H", 1, [float], ["x"]],
|
||||||
|
"V": ["V", 1, [float], ["y"]],
|
||||||
|
"C": [
|
||||||
|
"C",
|
||||||
|
6,
|
||||||
|
[float, float, float, float, float, float],
|
||||||
|
["x", "y", "x", "y", "x", "y"],
|
||||||
|
],
|
||||||
|
"S": ["S", 4, [float, float, float, float], ["x", "y", "x", "y"]],
|
||||||
|
"Q": ["Q", 4, [float, float, float, float], ["x", "y", "x", "y"]],
|
||||||
|
"T": ["T", 2, [float, float], ["x", "y"]],
|
||||||
|
"A": [
|
||||||
|
"A",
|
||||||
|
7,
|
||||||
|
[float, float, float, int, int, float, float],
|
||||||
|
["r", "r", "a", 0, "s", "x", "y"],
|
||||||
|
],
|
||||||
|
"Z": ["L", 0, [], []],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def parsePath(d):
|
||||||
|
"""element.path.to_arrays()"""
|
||||||
|
return Path(d).to_arrays()
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def formatPath(a):
|
||||||
|
"""str(element.path) or str(Path(array))"""
|
||||||
|
return str(Path(a))
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def translatePath(p, x, y):
|
||||||
|
"""Path(array).translate(x, y)"""
|
||||||
|
p[:] = Path(p).translate(x, y).to_arrays()
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def scalePath(p, x, y):
|
||||||
|
"""Path(array).scale(x, y)"""
|
||||||
|
p[:] = Path(p).scale(x, y).to_arrays()
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def rotatePath(p, a, cx=0, cy=0):
|
||||||
|
"""Path(array).rotate(angle_degrees, (center_x, center_y))"""
|
||||||
|
p[:] = Path(p).rotate(math.degrees(a), (cx, cy)).to_arrays()
|
||||||
55
deps/inkex/deprecated-simple/simplestyle.py
vendored
Normal file
55
deps/inkex/deprecated-simple/simplestyle.py
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
# COPYRIGHT
|
||||||
|
"""DOCSTRING"""
|
||||||
|
|
||||||
|
import inkex
|
||||||
|
from inkex.colors.spaces.named import _COLORS as svgcolors
|
||||||
|
from inkex.deprecated import deprecate
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def parseStyle(s):
|
||||||
|
"""dict(inkex.Style.parse_str(s))"""
|
||||||
|
return dict(inkex.Style.parse_str(s))
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def formatStyle(a):
|
||||||
|
"""str(inkex.Style(a))"""
|
||||||
|
return str(inkex.Style(a))
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def isColor(c):
|
||||||
|
"""inkex.colors.is_color(c)"""
|
||||||
|
return inkex.colors.is_color(c)
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def parseColor(c):
|
||||||
|
"""inkex.Color(c).to_rgb()"""
|
||||||
|
return tuple(inkex.Color(c).to_rgb())
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def formatColoria(a):
|
||||||
|
"""str(inkex.Color(a))"""
|
||||||
|
return str(inkex.ColorRGB(a))
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def formatColorfa(a):
|
||||||
|
"""str(inkex.Color(a))"""
|
||||||
|
return str(inkex.ColorRGB([b * 255 for b in a]))
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def formatColor3i(r, g, b):
|
||||||
|
"""str(inkex.Color((r, g, b)))"""
|
||||||
|
return str(inkex.ColorRGB((r, g, b)))
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def formatColor3f(r, g, b):
|
||||||
|
"""str(inkex.Color((r, g, b)))"""
|
||||||
|
return str(inkex.ColorRGB((r * 255, g * 255, b * 255)))
|
||||||
122
deps/inkex/deprecated-simple/simpletransform.py
vendored
Normal file
122
deps/inkex/deprecated-simple/simpletransform.py
vendored
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Depreicated simpletransform replacements with documentation
|
||||||
|
"""
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from inkex.deprecated import deprecate
|
||||||
|
from inkex.transforms import Transform, BoundingBox, cubic_extrema
|
||||||
|
from inkex.paths import Path
|
||||||
|
|
||||||
|
import inkex, cubicsuperpath
|
||||||
|
|
||||||
|
|
||||||
|
def _lists(mat):
|
||||||
|
return [list(row) for row in mat]
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def parseTransform(transf, mat=None):
|
||||||
|
"""Transform(str).matrix"""
|
||||||
|
t = Transform(transf)
|
||||||
|
if mat is not None:
|
||||||
|
t = Transform(mat) @ t
|
||||||
|
return _lists(t.matrix)
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def formatTransform(mat):
|
||||||
|
"""str(Transform(mat))"""
|
||||||
|
if len(mat) == 3:
|
||||||
|
warnings.warn("3x3 matrices not suported")
|
||||||
|
mat = mat[:2]
|
||||||
|
return str(Transform(mat))
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def invertTransform(mat):
|
||||||
|
"""-Transform(mat)"""
|
||||||
|
return _lists((-Transform(mat)).matrix)
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def composeTransform(mat1, mat2):
|
||||||
|
"""Transform(M1) * Transform(M2)"""
|
||||||
|
return _lists((Transform(mat1) @ Transform(mat2)).matrix)
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def composeParents(node, mat):
|
||||||
|
"""elem.composed_transform() or elem.transform * Transform(mat)"""
|
||||||
|
return (node.transform @ Transform(mat)).matrix
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def applyTransformToNode(mat, node):
|
||||||
|
"""elem.transform = Transform(mat) * elem.transform"""
|
||||||
|
node.transform = Transform(mat) @ node.transform
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def applyTransformToPoint(mat, pt):
|
||||||
|
"""Transform(mat).apply_to_point(pt)"""
|
||||||
|
pt2 = Transform(mat).apply_to_point(pt)
|
||||||
|
# Apply in place as original method was modifying arrays in place.
|
||||||
|
# but don't do this in your code! This is not good code design.
|
||||||
|
pt[0] = pt2[0]
|
||||||
|
pt[1] = pt2[1]
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def applyTransformToPath(mat, path):
|
||||||
|
"""Path(path).transform(mat)"""
|
||||||
|
return Path(path).transform(Transform(mat)).to_arrays()
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def fuseTransform(node):
|
||||||
|
"""node.apply_transform()"""
|
||||||
|
return node.apply_transform()
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def boxunion(b1, b2):
|
||||||
|
"""list(BoundingBox(b1) + BoundingBox(b2))"""
|
||||||
|
bbox = BoundingBox(b1[:2], b1[2:]) + BoundingBox(b2[:2], b2[2:])
|
||||||
|
return bbox.x.minimum, bbox.x.maximum, bbox.y.minimum, bbox.y.maximum
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def roughBBox(path):
|
||||||
|
"""list(Path(path)).bounding_box())"""
|
||||||
|
bbox = Path(path).bounding_box()
|
||||||
|
return bbox.x.minimum, bbox.x.maximum, bbox.y.minimum, bbox.y.maximum
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def refinedBBox(path):
|
||||||
|
"""list(Path(path)).bounding_box())"""
|
||||||
|
bbox = Path(path).bounding_box()
|
||||||
|
return bbox.x.minimum, bbox.x.maximum, bbox.y.minimum, bbox.y.maximum
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def cubicExtrema(y0, y1, y2, y3):
|
||||||
|
"""from inkex.transforms import cubic_extrema"""
|
||||||
|
return cubic_extrema(y0, y1, y2, y3)
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def computeBBox(aList, mat=[[1, 0, 0], [0, 1, 0]]):
|
||||||
|
"""sum([node.bounding_box() for node in aList])"""
|
||||||
|
return sum([node.bounding_box() for node in aList], None)
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def computePointInNode(pt, node, mat=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
|
||||||
|
"""(-Transform(node.transform * mat)).apply_to_point(pt)"""
|
||||||
|
return (-Transform(node.transform * mat)).apply_to_point(pt)
|
||||||
3
deps/inkex/deprecated/__init__.py
vendored
Normal file
3
deps/inkex/deprecated/__init__.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .main import *
|
||||||
|
from .meta import deprecate, _deprecated
|
||||||
|
from .deprecatedeffect import DeprecatedEffect, Effect
|
||||||
BIN
deps/inkex/deprecated/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/deprecated/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/deprecated/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/deprecated/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/deprecated/__pycache__/deprecatedeffect.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/deprecated/__pycache__/deprecatedeffect.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/deprecated/__pycache__/deprecatedeffect.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/deprecated/__pycache__/deprecatedeffect.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/deprecated/__pycache__/main.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/deprecated/__pycache__/main.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/deprecated/__pycache__/main.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/deprecated/__pycache__/main.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/deprecated/__pycache__/meta.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/deprecated/__pycache__/meta.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/deprecated/__pycache__/meta.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/deprecated/__pycache__/meta.cpython-313.pyc
vendored
Normal file
Binary file not shown.
304
deps/inkex/deprecated/deprecatedeffect.py
vendored
Normal file
304
deps/inkex/deprecated/deprecatedeffect.py
vendored
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2018 - Martin Owens <doctormo@mgail.com>
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Deprecation functionality for the pre-1.0 Inkex main effect class.
|
||||||
|
"""
|
||||||
|
#
|
||||||
|
# We ignore a lot of pylint warnings here:
|
||||||
|
#
|
||||||
|
# pylint: disable=invalid-name,unused-argument,missing-docstring,too-many-public-methods
|
||||||
|
#
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
|
||||||
|
from .. import utils
|
||||||
|
from .. import base
|
||||||
|
from ..base import SvgThroughMixin, InkscapeExtension
|
||||||
|
from ..localization import inkex_gettext as _
|
||||||
|
from .meta import _deprecated
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecatedEffect:
|
||||||
|
"""An Inkscape effect, takes SVG in and outputs SVG, providing a deprecated layer"""
|
||||||
|
|
||||||
|
options = argparse.Namespace()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._doc_ids = None
|
||||||
|
self._args = None
|
||||||
|
|
||||||
|
# These are things we reference in the deprecated code, they are provided
|
||||||
|
# by the new effects code, but we want to keep this as a Mixin so these
|
||||||
|
# items will keep pylint happy and let use check our code as we write.
|
||||||
|
if not hasattr(self, "svg"):
|
||||||
|
from ..elements import SvgDocumentElement
|
||||||
|
|
||||||
|
self.svg = SvgDocumentElement()
|
||||||
|
if not hasattr(self, "arg_parser"):
|
||||||
|
self.arg_parser = ArgumentParser()
|
||||||
|
if not hasattr(self, "run"):
|
||||||
|
self.run = self.affect
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _deprecated(
|
||||||
|
cls, name, msg=_("{} is deprecated and should be removed"), stack=3
|
||||||
|
):
|
||||||
|
"""Give the user a warning about their extension using a deprecated API"""
|
||||||
|
_deprecated(
|
||||||
|
msg.format("Effect." + name, cls=cls.__module__ + "." + cls.__name__),
|
||||||
|
stack=stack,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def OptionParser(self):
|
||||||
|
self._deprecated(
|
||||||
|
"OptionParser",
|
||||||
|
_(
|
||||||
|
"{} or `optparse` has been deprecated and replaced with `argparser`. "
|
||||||
|
"You must change `self.OptionParser.add_option` to "
|
||||||
|
"`self.arg_parser.add_argument`; the arguments are similar."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_option(self, *args, **kw):
|
||||||
|
# Convert type string into type method as needed
|
||||||
|
if "type" in kw:
|
||||||
|
kw["type"] = {
|
||||||
|
"string": str,
|
||||||
|
"int": int,
|
||||||
|
"float": float,
|
||||||
|
"inkbool": utils.Boolean,
|
||||||
|
}.get(kw["type"])
|
||||||
|
if kw.get("action", None) == "store":
|
||||||
|
# Default store action not required, removed.
|
||||||
|
kw.pop("action")
|
||||||
|
args = [arg for arg in args if arg != ""]
|
||||||
|
self.arg_parser.add_argument(*args, **kw)
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
self._deprecated(
|
||||||
|
"effect",
|
||||||
|
_(
|
||||||
|
"{} method is now a required method. It should "
|
||||||
|
"be created on {cls}, even if it does nothing."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_layer(self):
|
||||||
|
self._deprecated(
|
||||||
|
"current_layer",
|
||||||
|
_(
|
||||||
|
"{} is now a method in the SvgDocumentElement class. "
|
||||||
|
"Use `self.svg.get_current_layer()` instead."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return self.svg.get_current_layer()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def view_center(self):
|
||||||
|
self._deprecated(
|
||||||
|
"view_center",
|
||||||
|
_(
|
||||||
|
"{} is now a method in the SvgDocumentElement class. "
|
||||||
|
"Use `self.svg.get_center_position()` instead."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return self.svg.namedview.center
|
||||||
|
|
||||||
|
@property
|
||||||
|
def selected(self):
|
||||||
|
self._deprecated(
|
||||||
|
"selected",
|
||||||
|
_(
|
||||||
|
"{} is now a dict in the SvgDocumentElement class. "
|
||||||
|
"Use `self.svg.selected`."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return {elem.get("id"): elem for elem in self.svg.selected}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def doc_ids(self):
|
||||||
|
self._deprecated(
|
||||||
|
"doc_ids",
|
||||||
|
_(
|
||||||
|
"{} is now a method in the SvgDocumentElement class. "
|
||||||
|
"Use `self.svg.get_ids()` instead."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if self._doc_ids is None:
|
||||||
|
self._doc_ids = dict.fromkeys(self.svg.get_ids())
|
||||||
|
return self._doc_ids
|
||||||
|
|
||||||
|
def getselected(self):
|
||||||
|
self._deprecated("getselected", _("{} has been removed"))
|
||||||
|
|
||||||
|
def getElementById(self, eid):
|
||||||
|
self._deprecated(
|
||||||
|
"getElementById",
|
||||||
|
_(
|
||||||
|
"{} is now a method in the SvgDocumentElement class. "
|
||||||
|
"Use `self.svg.getElementById(eid)` instead."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return self.svg.getElementById(eid)
|
||||||
|
|
||||||
|
def xpathSingle(self, xpath):
|
||||||
|
self._deprecated(
|
||||||
|
"xpathSingle",
|
||||||
|
_(
|
||||||
|
"{} is now a new method in the SvgDocumentElement class. "
|
||||||
|
"Use `self.svg.getElement(path)` instead."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return self.svg.getElement(xpath)
|
||||||
|
|
||||||
|
def getParentNode(self, node):
|
||||||
|
self._deprecated(
|
||||||
|
"getParentNode",
|
||||||
|
_("{} is no longer in use. Use the lxml `.getparent()` method instead."),
|
||||||
|
)
|
||||||
|
return node.getparent()
|
||||||
|
|
||||||
|
def getNamedView(self):
|
||||||
|
self._deprecated(
|
||||||
|
"getNamedView",
|
||||||
|
_(
|
||||||
|
"{} is now a property of the SvgDocumentElement class. "
|
||||||
|
"Use `self.svg.namedview` to access this element."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return self.svg.namedview
|
||||||
|
|
||||||
|
def createGuide(self, posX, posY, angle):
|
||||||
|
from ..elements import Guide
|
||||||
|
|
||||||
|
self._deprecated(
|
||||||
|
"createGuide",
|
||||||
|
_(
|
||||||
|
"{} is now a method of the namedview element object. "
|
||||||
|
"Use `self.svg.namedview.add(Guide().move_to(x, y, a))` instead."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return self.svg.namedview.add(Guide().move_to(posX, posY, angle))
|
||||||
|
|
||||||
|
def affect(self, args=sys.argv[1:], output=True): # pylint: disable=dangerous-default-value
|
||||||
|
# We need a list as the default value to preserve backwards compatibility
|
||||||
|
self._deprecated(
|
||||||
|
"affect", _("{} is now `Effect.run()`. The `output` argument has changed.")
|
||||||
|
)
|
||||||
|
self._args = args[-1:]
|
||||||
|
return self.run(args=args)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def args(self):
|
||||||
|
self._deprecated("args", _("self.args[-1] is now self.options.input_file."))
|
||||||
|
return self._args
|
||||||
|
|
||||||
|
@property
|
||||||
|
def svg_file(self):
|
||||||
|
self._deprecated("svg_file", _("self.svg_file is now self.options.input_file."))
|
||||||
|
return self.options.input_file
|
||||||
|
|
||||||
|
def save_raw(self, ret):
|
||||||
|
# Derived class may implement "output()"
|
||||||
|
# Attention: 'cubify.py' implements __getattr__ -> hasattr(self, 'output')
|
||||||
|
# returns True
|
||||||
|
if hasattr(self.__class__, "output"):
|
||||||
|
self._deprecated("output", "Use `save()` or `save_raw()` instead.", stack=5)
|
||||||
|
return getattr(self, "output")()
|
||||||
|
return base.InkscapeExtension.save_raw(self, ret)
|
||||||
|
|
||||||
|
def uniqueId(self, old_id, make_new_id=True):
|
||||||
|
self._deprecated(
|
||||||
|
"uniqueId",
|
||||||
|
_(
|
||||||
|
"{} is now a method in the SvgDocumentElement class. "
|
||||||
|
" Use `self.svg.get_unique_id(old_id)` instead."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return self.svg.get_unique_id(old_id)
|
||||||
|
|
||||||
|
def getDocumentWidth(self):
|
||||||
|
self._deprecated(
|
||||||
|
"getDocumentWidth",
|
||||||
|
_(
|
||||||
|
"{} is now a property of the SvgDocumentElement class. "
|
||||||
|
"Use `self.svg.width` instead."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return self.svg.get("width")
|
||||||
|
|
||||||
|
def getDocumentHeight(self):
|
||||||
|
self._deprecated(
|
||||||
|
"getDocumentHeight",
|
||||||
|
_(
|
||||||
|
"{} is now a property of the SvgDocumentElement class. "
|
||||||
|
"Use `self.svg.height` instead."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return self.svg.get("height")
|
||||||
|
|
||||||
|
def getDocumentUnit(self):
|
||||||
|
self._deprecated(
|
||||||
|
"getDocumentUnit",
|
||||||
|
_(
|
||||||
|
"{} is now a property of the SvgDocumentElement class. "
|
||||||
|
"Use `self.svg.unit` instead."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return self.svg.unit
|
||||||
|
|
||||||
|
def unittouu(self, string):
|
||||||
|
self._deprecated(
|
||||||
|
"unittouu",
|
||||||
|
_(
|
||||||
|
"{} is now a method in the SvgDocumentElement class. "
|
||||||
|
"Use `self.svg.unittouu(str)` instead."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return self.svg.unittouu(string)
|
||||||
|
|
||||||
|
def uutounit(self, val, unit):
|
||||||
|
self._deprecated(
|
||||||
|
"uutounit",
|
||||||
|
_(
|
||||||
|
"{} is now a method in the SvgDocumentElement class. "
|
||||||
|
"Use `self.svg.uutounit(value, unit)` instead."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return self.svg.uutounit(val, unit)
|
||||||
|
|
||||||
|
def addDocumentUnit(self, value):
|
||||||
|
self._deprecated(
|
||||||
|
"addDocumentUnit",
|
||||||
|
_(
|
||||||
|
"{} is now a method in the SvgDocumentElement class. "
|
||||||
|
"Use `self.svg.add_unit(value)` instead."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return self.svg.add_unit(value)
|
||||||
|
|
||||||
|
|
||||||
|
class Effect(SvgThroughMixin, DeprecatedEffect, InkscapeExtension):
|
||||||
|
"""An Inkscape effect, takes SVG in and outputs SVG"""
|
||||||
237
deps/inkex/deprecated/main.py
vendored
Normal file
237
deps/inkex/deprecated/main.py
vendored
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2018 - Martin Owens <doctormo@mgail.com>
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Provide some documentation to existing extensions about why they're failing.
|
||||||
|
"""
|
||||||
|
#
|
||||||
|
# We ignore a lot of pylint warnings here:
|
||||||
|
#
|
||||||
|
# pylint: disable=invalid-name,unused-argument,missing-docstring,too-many-public-methods
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import warnings
|
||||||
|
import argparse
|
||||||
|
import cssselect
|
||||||
|
|
||||||
|
from ..transforms import Transform
|
||||||
|
from .. import utils
|
||||||
|
from .. import units
|
||||||
|
from ..elements._base import BaseElement, ShapeElement
|
||||||
|
from ..elements._selected import ElementList
|
||||||
|
from .meta import deprecate, _deprecated
|
||||||
|
from ..styles import ConditionalStyle, Style
|
||||||
|
from ..colors import Color
|
||||||
|
|
||||||
|
warnings.simplefilter("default")
|
||||||
|
# To load each of the deprecated sub-modules (the ones without a namespace)
|
||||||
|
# we will add the directory to our pythonpath so older scripts can find them
|
||||||
|
|
||||||
|
INKEX_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
SIMPLE_DIR = os.path.join(INKEX_DIR, "deprecated-simple")
|
||||||
|
|
||||||
|
if os.path.isdir(SIMPLE_DIR):
|
||||||
|
sys.path.append(SIMPLE_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecatedDict(dict):
|
||||||
|
@deprecate
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return super().__getitem__(key)
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def __iter__(self):
|
||||||
|
return super().__iter__()
|
||||||
|
|
||||||
|
|
||||||
|
# legacy inkex members
|
||||||
|
|
||||||
|
|
||||||
|
class lazyproxy:
|
||||||
|
"""Proxy, use as decorator on a function with provides the wrapped object.
|
||||||
|
The decorated function is called when a member is accessed on the proxy.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, getwrapped):
|
||||||
|
"""
|
||||||
|
:param getwrapped: Callable which returns the wrapped object
|
||||||
|
"""
|
||||||
|
self._getwrapped = getwrapped
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self._getwrapped(), name)
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
return self._getwrapped()(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@lazyproxy
|
||||||
|
def localize():
|
||||||
|
_deprecated("inkex.localize was moved to inkex.localization.localize.", stack=3)
|
||||||
|
from ..localization import localize as wrapped
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
def are_near_relative(a, b, eps):
|
||||||
|
_deprecated(
|
||||||
|
"inkex.are_near_relative was moved to inkex.units.are_near_relative", stack=2
|
||||||
|
)
|
||||||
|
return units.are_near_relative(a, b, eps)
|
||||||
|
|
||||||
|
|
||||||
|
def debug(what):
|
||||||
|
_deprecated("inkex.debug was moved to inkex.utils.debug.", stack=2)
|
||||||
|
return utils.debug(what)
|
||||||
|
|
||||||
|
|
||||||
|
# legacy inkex members <= 0.48.x
|
||||||
|
|
||||||
|
|
||||||
|
def unittouu(string):
|
||||||
|
_deprecated(
|
||||||
|
"inkex.unittouu is now a method in the SvgDocumentElement class. "
|
||||||
|
"Use `self.svg.unittouu(str)` instead.",
|
||||||
|
stack=2,
|
||||||
|
)
|
||||||
|
return units.convert_unit(string, "px")
|
||||||
|
|
||||||
|
|
||||||
|
# optparse.Values.ensure_value
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_value(self, attr, value):
|
||||||
|
_deprecated("Effect().options.ensure_value was removed.", stack=2)
|
||||||
|
if getattr(self, attr, None) is None:
|
||||||
|
setattr(self, attr, value)
|
||||||
|
return getattr(self, attr)
|
||||||
|
|
||||||
|
|
||||||
|
argparse.Namespace.ensure_value = ensure_value # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def zSort(inNode, idList):
|
||||||
|
"""self.svg.get_z_selected()"""
|
||||||
|
sortedList = []
|
||||||
|
theid = inNode.get("id")
|
||||||
|
if theid in idList:
|
||||||
|
sortedList.append(theid)
|
||||||
|
for child in inNode:
|
||||||
|
if len(sortedList) == len(idList):
|
||||||
|
break
|
||||||
|
sortedList += zSort(child, idList)
|
||||||
|
return sortedList
|
||||||
|
|
||||||
|
|
||||||
|
# This can't be handled as a mixin class because of circular importing.
|
||||||
|
def description(self, value):
|
||||||
|
"""Use elem.desc = value"""
|
||||||
|
self.desc = value
|
||||||
|
|
||||||
|
|
||||||
|
BaseElement.description = deprecate(description, "1.1")
|
||||||
|
|
||||||
|
|
||||||
|
def composed_style(element: ShapeElement):
|
||||||
|
"""Calculate the final styles applied to this element
|
||||||
|
This function has been deprecated in favor of BaseElement.specified_style()"""
|
||||||
|
return element.specified_style()
|
||||||
|
|
||||||
|
|
||||||
|
ShapeElement.composed_style = deprecate(composed_style, "1.2")
|
||||||
|
|
||||||
|
|
||||||
|
def paint_order(selection: ElementList):
|
||||||
|
"""Use :func:`rendering_order`"""
|
||||||
|
return selection.rendering_order()
|
||||||
|
|
||||||
|
|
||||||
|
ElementList.paint_order = deprecate(paint_order, "1.2") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def transform_imul(self, matrix):
|
||||||
|
"""Use @= operator instead"""
|
||||||
|
return self.__imatmul__(matrix)
|
||||||
|
|
||||||
|
|
||||||
|
def transform_mul(self, matrix):
|
||||||
|
"""Use @ operator instead"""
|
||||||
|
return self.__matmul__(matrix)
|
||||||
|
|
||||||
|
|
||||||
|
Transform.__imul__ = deprecate(transform_imul, "1.2") # type: ignore
|
||||||
|
Transform.__mul__ = deprecate(transform_mul, "1.2") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def to_xpath(self):
|
||||||
|
"""Depending on whether you need to apply the rule to an invididual element
|
||||||
|
or find all matches in a subtree, use
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
style.matches(element)
|
||||||
|
style.all_matches(subtree)
|
||||||
|
"""
|
||||||
|
return "|".join(self.to_xpaths())
|
||||||
|
|
||||||
|
|
||||||
|
def to_xpaths(self):
|
||||||
|
"""Depending on whether you need to apply the rule to an invididual element
|
||||||
|
or find all matches in a subtree, use
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
style.matches(element)
|
||||||
|
style.all_matches(subtree)
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
for rule in self.rules:
|
||||||
|
ret = (
|
||||||
|
cssselect.HTMLTranslator().selector_to_xpath(cssselect.parse(str(rule))[0])
|
||||||
|
+ " "
|
||||||
|
)
|
||||||
|
ret = re.compile(r"(::|\/)([a-z]+)(?=\W)(?!-)").sub(r"\1svg:\2", ret)
|
||||||
|
result.append(ret.strip())
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
ConditionalStyle.to_xpath = deprecate(to_xpath, "1.4") # type: ignore
|
||||||
|
ConditionalStyle.to_xpaths = deprecate(to_xpaths, "1.4") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def apply_shorthands(self):
|
||||||
|
"""Apply all shorthands in this style. Shorthands are now simplified automatically,
|
||||||
|
so this method does nothing"""
|
||||||
|
|
||||||
|
|
||||||
|
Style.apply_shorthands = deprecate(apply_shorthands, "1.4") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def to_rgba(self, alpha=1.0):
|
||||||
|
"""
|
||||||
|
Opacity is now controlled via alpha property regardless of color space being used.
|
||||||
|
"""
|
||||||
|
ret = self.to_rgb()
|
||||||
|
ret.alpha = float(alpha)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
Color.to_rgba = deprecate(to_rgba, "1.5") # type: ignore
|
||||||
109
deps/inkex/deprecated/meta.py
vendored
Normal file
109
deps/inkex/deprecated/meta.py
vendored
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# Copyright (C) 2018 - Martin Owens <doctormo@gmail.com>
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Deprecation functionality which does not require imports from Inkex.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
import warnings
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
try:
|
||||||
|
DEPRECATION_LEVEL = int(os.environ.get("INKEX_DEPRECATION_LEVEL", 1))
|
||||||
|
except ValueError:
|
||||||
|
DEPRECATION_LEVEL = 1
|
||||||
|
|
||||||
|
|
||||||
|
def _deprecated(msg, stack=2, level=DEPRECATION_LEVEL):
|
||||||
|
"""Internal method for raising a deprecation warning"""
|
||||||
|
if level > 1:
|
||||||
|
msg += " ; ".join(traceback.format_stack())
|
||||||
|
if level:
|
||||||
|
warnings.warn(msg, category=DeprecationWarning, stacklevel=stack + 1)
|
||||||
|
|
||||||
|
|
||||||
|
def deprecate(func, version: Optional[str] = None):
|
||||||
|
r"""Function decorator for deprecation functions which have a one-liner
|
||||||
|
equivalent in the new API. The one-liner has to passed as a string
|
||||||
|
to the decorator.
|
||||||
|
|
||||||
|
>>> @deprecate
|
||||||
|
>>> def someOldFunction(*args):
|
||||||
|
>>> '''Example replacement code someNewFunction('foo', ...)'''
|
||||||
|
>>> someNewFunction('foo', *args)
|
||||||
|
|
||||||
|
Or if the args API is the same:
|
||||||
|
|
||||||
|
>>> someOldFunction = deprecate(someNewFunction)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _inner(*args, **kwargs):
|
||||||
|
_deprecated(f"{func.__module__}.{func.__name__} -> {func.__doc__}", stack=2)
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
_inner.__name__ = func.__name__
|
||||||
|
if func.__doc__:
|
||||||
|
if version is None:
|
||||||
|
_inner.__doc__ = "Deprecated -> " + func.__doc__
|
||||||
|
else:
|
||||||
|
_inner.__doc__ = f"""{func.__doc__}\n\n.. deprecated:: {version}\n"""
|
||||||
|
return _inner
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecatedSvgMixin:
|
||||||
|
"""Mixin which adds deprecated API elements to the SvgDocumentElement"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def selected(self):
|
||||||
|
"""svg.selection"""
|
||||||
|
return self.selection
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def set_selected(self, *ids):
|
||||||
|
r"""svg.selection.set(\*ids)"""
|
||||||
|
return self.selection.set(*ids)
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def get_z_selected(self):
|
||||||
|
"""svg.selection.rendering_order()"""
|
||||||
|
return self.selection.rendering_order()
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def get_selected(self, *types):
|
||||||
|
r"""svg.selection.filter(\*types).values()"""
|
||||||
|
return self.selection.filter(*types).values()
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def get_selected_or_all(self, *types):
|
||||||
|
"""Set select_all = True in extension class"""
|
||||||
|
if not self.selection:
|
||||||
|
self.selection.set_all()
|
||||||
|
return self.selection.filter(*types)
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def get_selected_bbox(self):
|
||||||
|
"""selection.bounding_box()"""
|
||||||
|
return self.selection.bounding_box()
|
||||||
|
|
||||||
|
@deprecate
|
||||||
|
def get_first_selected(self, *types):
|
||||||
|
r"""selection.filter(\*types).first() or [0] if you'd like an error"""
|
||||||
|
return self.selection.filter(*types).first()
|
||||||
56
deps/inkex/elements/__init__.py
vendored
Normal file
56
deps/inkex/elements/__init__.py
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
"""
|
||||||
|
Element based interface provides the bulk of features that allow you to
|
||||||
|
interact directly with the SVG xml interface.
|
||||||
|
|
||||||
|
See the documentation for each of the elements for details on how it works.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ._utils import addNS, NSS
|
||||||
|
from ._parser import SVG_PARSER, load_svg
|
||||||
|
from ._base import ShapeElement, BaseElement
|
||||||
|
from ._svg import SvgDocumentElement
|
||||||
|
from ._groups import Group, Layer, Anchor, Marker, ClipPath
|
||||||
|
from ._polygons import PathElement, Polyline, Polygon, Line, Rectangle, Circle, Ellipse
|
||||||
|
from ._text import (
|
||||||
|
FlowRegion,
|
||||||
|
FlowRoot,
|
||||||
|
FlowPara,
|
||||||
|
FlowDiv,
|
||||||
|
FlowSpan,
|
||||||
|
TextElement,
|
||||||
|
TextPath,
|
||||||
|
Tspan,
|
||||||
|
SVGfont,
|
||||||
|
FontFace,
|
||||||
|
Glyph,
|
||||||
|
MissingGlyph,
|
||||||
|
)
|
||||||
|
from ._use import Symbol, Use
|
||||||
|
from ._meta import (
|
||||||
|
Defs,
|
||||||
|
StyleElement,
|
||||||
|
Script,
|
||||||
|
Desc,
|
||||||
|
Title,
|
||||||
|
NamedView,
|
||||||
|
Guide,
|
||||||
|
Metadata,
|
||||||
|
ForeignObject,
|
||||||
|
Switch,
|
||||||
|
Grid,
|
||||||
|
Page,
|
||||||
|
)
|
||||||
|
from ._filters import (
|
||||||
|
Filter,
|
||||||
|
Pattern,
|
||||||
|
Mask,
|
||||||
|
Gradient,
|
||||||
|
LinearGradient,
|
||||||
|
RadialGradient,
|
||||||
|
PathEffect,
|
||||||
|
Stop,
|
||||||
|
MeshGradient,
|
||||||
|
MeshRow,
|
||||||
|
MeshPatch,
|
||||||
|
)
|
||||||
|
from ._image import Image
|
||||||
BIN
deps/inkex/elements/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/elements/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/elements/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/elements/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user