Compare commits
12 Commits
cf789ba0b8
...
knoxmakers
| Author | SHA1 | Date | |
|---|---|---|---|
| a5ddc33a27 | |||
| a9c87f2adf | |||
| 5692f4bb08 | |||
| 9af24d49ee | |||
| 7211fd44ef | |||
| b72b468ad4 | |||
| 8a126fec7d | |||
| e0dc82c22d | |||
| c2fa54d3b4 | |||
| 7597f2f156 | |||
| 822898e4a3 | |||
| 22a55924e3 |
@@ -45,8 +45,8 @@ jobs:
|
||||
git push origin HEAD:main
|
||||
|
||||
# Create a unique tag for this bundle run
|
||||
DATETIME="$(date -u +%Y%m%d-%H%M%S)"
|
||||
TAG="knox-makers-inkscape-${DATETIME}"
|
||||
DATETIME="$(date -u +%Y%m%d)"
|
||||
TAG="knoxmakers-inkscape-${DATETIME}"
|
||||
SHA="$(git rev-parse HEAD)"
|
||||
|
||||
git tag -a "$TAG" -m "Automated bundle: $TAG ($SHA)"
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
CREATE_JSON="$(jq -n \
|
||||
--arg tag "$TAG" \
|
||||
--arg name "$TAG" \
|
||||
--arg body "Automated bundle release.\n\nExcludes: scripts/, .gitea/\nCommit: $SHA\nUTC: $(date -u +'%Y-%m-%d %H:%M:%S')" \
|
||||
--arg body "Automated bundle release." \
|
||||
'{tag_name:$tag, name:$name, body:$body, draft:false, prerelease:false}')"
|
||||
|
||||
RELEASE_RESP="$(curl -fsS -X POST "$API_CREATE" \
|
||||
|
||||
45
.gitea/workflows/version.yml
Normal file
45
.gitea/workflows/version.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
name: version
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
update-version:
|
||||
runs-on: [ubuntu-24.04]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf .git || true
|
||||
rm -rf ./* ./.??* || true
|
||||
git clone --depth 1 https://git.knoxmakers.org/KnoxMakers/knoxmakers-inkscape.git .
|
||||
git checkout main
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config user.name "haxbot"
|
||||
git config user.email "haxbot@knoxmakers.sh"
|
||||
|
||||
- name: Write version file
|
||||
run: |
|
||||
echo "${{ gitea.event.release.name }}" > version
|
||||
|
||||
- name: Commit and push version file
|
||||
env:
|
||||
HAXBOT_TOKEN: ${{ secrets.HAXBOT_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
git add version
|
||||
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to version file"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit -m "version: ${{ gitea.event.release.name }}"
|
||||
|
||||
REPO_URL="https://x-access-token:${HAXBOT_TOKEN}@git.knoxmakers.org/KnoxMakers/knoxmakers-inkscape.git"
|
||||
git remote set-url origin "$REPO_URL"
|
||||
git push origin HEAD:main
|
||||
25
README.md
25
README.md
@@ -1,2 +1,27 @@
|
||||
# knoxmakers-inkscape
|
||||
|
||||
A bundled collection of Inkscape extensions for Knox Makers makerspace.
|
||||
|
||||
## Installation
|
||||
|
||||
Download the latest release zip and extract into to your Inkscape extensions directory:
|
||||
|
||||
- **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\`
|
||||
|
||||
## Included Extensions
|
||||
|
||||
- **botbox3000** - Box generator for laser cutting
|
||||
- **km-living-hinge** - Living hinge pattern generator
|
||||
- **km-plot** - Detect and send designs to serial HPGL plotters/vinyl cutters
|
||||
- **km-hatch** - Hatching/fill patterns
|
||||
- **km-hershey** - Single Line text for plotting/engraving
|
||||
|
||||
Extensions appear under **Extensions > Knox Makers** in Inkscape.
|
||||
|
||||
## Releases
|
||||
|
||||
Automated bundles are created daily when upstream repositories have changes.
|
||||
|
||||
@@ -182,7 +182,7 @@ class Boxbot(inkex.EffectExtension):
|
||||
self.top_hole_inset = PathElement()
|
||||
self.top_hole_inset.set_id(self.svg.get_unique_id("top_hole_inset"))
|
||||
self.top_hole_inset.set('d', top_hole_inset_d)
|
||||
self.top_hole_inset.style = self.CUT_OUTER_STYLE
|
||||
self.top_hole_inset.style = self.CUT_INNER_STYLE
|
||||
top_tabs_group.append(self.top_hole_inset)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
0
extensions/botbox3000/deps/inkex/deprecated-simple/run_command.py
Executable file → Normal file
0
extensions/botbox3000/deps/inkex/deprecated-simple/run_command.py
Executable file → Normal file
BIN
extensions/botbox3000/doc/logo.png
Normal file
BIN
extensions/botbox3000/doc/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 148 KiB |
@@ -18,5 +18,5 @@ An Inkscape extension that fills closed shapes with parallel hatch lines. The ex
|
||||
|
||||
Inspiration, examples, and code came from:
|
||||
|
||||
- **Hatch Fill** from Evil Mad Scientist Laboratories and their EggBot extensions
|
||||
- **Hatch Fill** from Evil Mad Scientist Laboratories and their EggBot extensions
|
||||
https://github.com/evil-mad/EggBot/
|
||||
|
||||
1
extensions/km-hershey/.upstream_branch
Normal file
1
extensions/km-hershey/.upstream_branch
Normal file
@@ -0,0 +1 @@
|
||||
main
|
||||
1
extensions/km-hershey/.upstream_commit
Normal file
1
extensions/km-hershey/.upstream_commit
Normal file
@@ -0,0 +1 @@
|
||||
b9b0c988e4078c87c7b78820f92fad58dcddaf00
|
||||
1
extensions/km-hershey/.upstream_url
Normal file
1
extensions/km-hershey/.upstream_url
Normal file
@@ -0,0 +1 @@
|
||||
https://git.knoxmakers.org/KnoxMakers/km-hershey.git
|
||||
668
extensions/km-hershey/LICENSE
Normal file
668
extensions/km-hershey/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
extensions/km-hershey/README.md
Normal file
31
extensions/km-hershey/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
extensions/km-hershey/deps/inkex/__init__.py
Normal file
33
extensions/km-hershey/deps/inkex/__init__.py
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.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
567
extensions/km-hershey/deps/inkex/base.py
Normal file
567
extensions/km-hershey/deps/inkex/base.py
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
extensions/km-hershey/deps/inkex/bezier.py
Normal file
582
extensions/km-hershey/deps/inkex/bezier.py
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
extensions/km-hershey/deps/inkex/colors/__init__.py
Normal file
49
extensions/km-hershey/deps/inkex/colors/__init__.py
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 *
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
295
extensions/km-hershey/deps/inkex/colors/color.py
Normal file
295
extensions/km-hershey/deps/inkex/colors/color.py
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
extensions/km-hershey/deps/inkex/colors/converters.py
Normal file
122
extensions/km-hershey/deps/inkex/colors/converters.py
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
extensions/km-hershey/deps/inkex/colors/spaces/__init__.py
Normal file
11
extensions/km-hershey/deps/inkex/colors/spaces/__init__.py
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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
95
extensions/km-hershey/deps/inkex/colors/spaces/cms.py
Normal file
95
extensions/km-hershey/deps/inkex/colors/spaces/cms.py
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
extensions/km-hershey/deps/inkex/colors/spaces/cmyk.py
Normal file
81
extensions/km-hershey/deps/inkex/colors/spaces/cmyk.py
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
extensions/km-hershey/deps/inkex/colors/spaces/css.py
Normal file
139
extensions/km-hershey/deps/inkex/colors/spaces/css.py
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
extensions/km-hershey/deps/inkex/colors/spaces/hsl.py
Normal file
107
extensions/km-hershey/deps/inkex/colors/spaces/hsl.py
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
extensions/km-hershey/deps/inkex/colors/spaces/hsv.py
Normal file
88
extensions/km-hershey/deps/inkex/colors/spaces/hsv.py
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
extensions/km-hershey/deps/inkex/colors/spaces/named.py
Normal file
236
extensions/km-hershey/deps/inkex/colors/spaces/named.py
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
extensions/km-hershey/deps/inkex/colors/spaces/none.py
Normal file
55
extensions/km-hershey/deps/inkex/colors/spaces/none.py
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
extensions/km-hershey/deps/inkex/colors/spaces/rgb.py
Normal file
105
extensions/km-hershey/deps/inkex/colors/spaces/rgb.py
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
extensions/km-hershey/deps/inkex/colors/utils.py
Normal file
31
extensions/km-hershey/deps/inkex/colors/utils.py
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
extensions/km-hershey/deps/inkex/command.py
Normal file
347
extensions/km-hershey/deps/inkex/command.py
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
extensions/km-hershey/deps/inkex/css/__init__.py
Normal file
3
extensions/km-hershey/deps/inkex/css/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""CSS Processing module"""
|
||||
|
||||
from .compiler import CSSCompiler
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
483
extensions/km-hershey/deps/inkex/css/compiler.py
Normal file
483
extensions/km-hershey/deps/inkex/css/compiler.py
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
extensions/km-hershey/deps/inkex/css/parser.py
Normal file
548
extensions/km-hershey/deps/inkex/css/parser.py
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)})"
|
||||
@@ -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'
|
||||
|
||||
@@ -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."""
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
extensions/km-hershey/deps/inkex/deprecated-simple/ffgeom.py
Normal file
92
extensions/km-hershey/deps/inkex/deprecated-simple/ffgeom.py
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)
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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)))
|
||||
@@ -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
extensions/km-hershey/deps/inkex/deprecated/__init__.py
Normal file
3
extensions/km-hershey/deps/inkex/deprecated/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .main import *
|
||||
from .meta import deprecate, _deprecated
|
||||
from .deprecatedeffect import DeprecatedEffect, Effect
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
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