Initial commit.
28
CONTRIBUTING.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# How to Contribute
|
||||
|
||||
We'd love to accept your patches and contributions to this project. There are
|
||||
just a few small guidelines you need to follow.
|
||||
|
||||
## Contributor License Agreement
|
||||
|
||||
Contributions to this project must be accompanied by a Contributor License
|
||||
Agreement. You (or your employer) retain the copyright to your contribution;
|
||||
this simply gives us permission to use and redistribute your contributions as
|
||||
part of the project. Head over to <https://cla.developers.google.com/> to see
|
||||
your current agreements on file or to sign a new one.
|
||||
|
||||
You generally only need to submit a CLA once, so if you've already submitted one
|
||||
(even if it was for a different project), you probably don't need to do it
|
||||
again.
|
||||
|
||||
## Code reviews
|
||||
|
||||
All submissions, including submissions by project members, require review. We
|
||||
use GitHub pull requests for this purpose. Consult
|
||||
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
|
||||
information on using pull requests.
|
||||
|
||||
## Community Guidelines
|
||||
|
||||
This project follows [Google's Open Source Community
|
||||
Guidelines](https://opensource.google.com/conduct/).
|
202
LICENSE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
143
README.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# pybadges
|
||||
|
||||
pybadges is a Python library and command line tools that allows you to create
|
||||
Git-hub styles badges as SVG images. For example:
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
The aesthetics of the generated badges matches the visual design of Shields badges
|
||||
[specification](https://github.com/badges/shields/blob/master/spec/SPECIFICATION.md).
|
||||
|
||||
The implementation of the library was heavily influenced by
|
||||
[Shields.io](https://github.com/badges/shields) and the JavaScript
|
||||
[gh-badges](https://github.com/badges/shields#using-the-badge-library) library.
|
||||
|
||||
## Getting Started
|
||||
|
||||
These instructions will get you a copy of the project up and running on your
|
||||
local machine for development and testing purposes. See deployment for notes on
|
||||
how to deploy the project on a live system.
|
||||
|
||||
### Installing
|
||||
|
||||
pybadges can be installed using [pip](https://pypi.org/project/pip/):
|
||||
|
||||
```sh
|
||||
pip install pybadges
|
||||
```
|
||||
|
||||
To test that installation was successful, try:
|
||||
```sh
|
||||
python -m pybadges --left-text=build --right-text=failure --right-color=#c00 --browser
|
||||
```
|
||||
|
||||
You will see a badge like this in your browser or other image viewer:
|
||||
|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
pybadges can be used both from the command line and as a Python library.
|
||||
|
||||
The command line interface is a great way to experiment with the API before
|
||||
writing Python code.
|
||||
|
||||
### Command line usage
|
||||
|
||||
Complete documentation of pybadges command arguments can be found using the help
|
||||
argument:
|
||||
|
||||
```sh
|
||||
python -m pybadges --help
|
||||
```
|
||||
|
||||
But the following usage demonstrates every interesting option:
|
||||
```sh
|
||||
python -m pybadges \
|
||||
--left-text=complete \
|
||||
--right-text=example \
|
||||
--left-color=green \
|
||||
--right-color=#fb3 \
|
||||
--left-link=http://www.complete.com/ \
|
||||
--right-link=http://www.example.com \
|
||||
--logo='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAD0lEQVQI12P4zwAD/xkYAA/+Af8iHnLUAAAAAElFTkSuQmCC' \
|
||||
--browser
|
||||
```
|
||||
|
||||

|
||||
|
||||
Note that the `--logo` option can include a regular URL:
|
||||
|
||||
```sh
|
||||
python -m pybadges \
|
||||
--left-text="python" \
|
||||
--right-text="3.2, 3.3, 3.4, 3.5, 3.6" \
|
||||
--whole-link="https://www.python.org/" \
|
||||
--browser \
|
||||
--logo='https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/python.svg'
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Library usage
|
||||
|
||||
pybadges is primarily meant to be used as a Python library.
|
||||
|
||||
```python
|
||||
from pybadges import badge
|
||||
s = badge(left_text='coverage', right_text='23%', right_color='red')
|
||||
# s is a string that contains the badge data as an svg image.
|
||||
print(s[:40]) # => <svg height="20" width="191.0" xmlns="ht
|
||||
```
|
||||
|
||||
The keyword arguments to `badge()` are identical to the command flags names
|
||||
described above except with keyword arguments using underscore instead of
|
||||
hyphen/minus (e.g. `--left-width` => `left_width=`)
|
||||
|
||||
### Caveats
|
||||
|
||||
- pybadges uses a pre-calculated table of text widths and
|
||||
[kerning](https://en.wikipedia.org/wiki/Kerning) distances
|
||||
(for western glyphs) to determine the size of the badge.
|
||||
So Eastern European languages may be rendered less well than
|
||||
Western European ones:
|
||||
|
||||

|
||||
|
||||
and glyphs not present in Deja Vu Sans (the default font) may
|
||||
be rendered very poorly:
|
||||
|
||||

|
||||
|
||||
- pybadges does not have any explicit support for languages that
|
||||
are written right-to-left (e.g. Arabic, Hebrew) and the displayed
|
||||
text direction may be incorrect:
|
||||
|
||||

|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
git clone TODO
|
||||
cd TODO
|
||||
python -m virtualenv py
|
||||
source py/bin/activate
|
||||
# Installs in edit mode and with development dependencies.
|
||||
pip install -e .[dev]
|
||||
nox
|
||||
```
|
||||
|
||||
If you'd like to contribute your changes back to pybadges, please read the
|
||||
[contributer guide.](CONTRIBUTING.md)
|
||||
|
||||
## Versioning
|
||||
|
||||
We use [SemVer](http://semver.org/) for versioning.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the Apache License - see the [LICENSE](LICENSE) file for details
|
||||
|
||||
This is not an officially supported Google product.
|
51
build_golden_images.py
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2018 The pybadge Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import pkg_resources
|
||||
|
||||
import pybadges
|
||||
|
||||
def generate_images(source_json_path, target_directory):
|
||||
os.makedirs(target_directory, exist_ok=True)
|
||||
with open(source_json_path) as f:
|
||||
examples = json.load(f)
|
||||
|
||||
for example in examples:
|
||||
filename = os.path.join(target_directory, example.pop('file_name'))
|
||||
with open(filename, 'w') as f:
|
||||
f.write(pybadges.badge(**example))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='generate a github-style badge given some text and colors')
|
||||
|
||||
parser.add_argument('--source-path',
|
||||
default=pkg_resources.resource_filename(__name__, 'tests/test-badges.json'),
|
||||
help='the text to show on the left-hand-side of the badge')
|
||||
|
||||
parser.add_argument('--destination-dir',
|
||||
default=pkg_resources.resource_filename(__name__, 'tests/golden-images'),
|
||||
help='the text to show on the left-hand-side of the badge')
|
||||
args = parser.parse_args()
|
||||
generate_images(args.source_path, args.destination_dir)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
50
nox.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Copyright 2018 The pybadge Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Nox config for running lint and unit tests."""
|
||||
|
||||
import nox
|
||||
|
||||
|
||||
@nox.session
|
||||
def lint(session):
|
||||
"""Run flake8.
|
||||
Returns a failure if flake8 finds linting errors or sufficiently
|
||||
serious code quality issues.
|
||||
"""
|
||||
session.interpreter = 'python3'
|
||||
session.install('flake8')
|
||||
session.run('flake8',
|
||||
'pypadges,tests')
|
||||
|
||||
|
||||
@nox.session
|
||||
def unit(session):
|
||||
"""Run the unit test suite.
|
||||
|
||||
Unit test files should be named like test_*.py and in the same directory
|
||||
as the file being tested.
|
||||
"""
|
||||
session.interpreter = 'python3'
|
||||
|
||||
# Install all test dependencies, then install this package in-place.
|
||||
# session.install('-r', 'requirements-test.txt')
|
||||
session.install('-e', '.[dev]')
|
||||
# Run py.test against the unit tests.
|
||||
session.run(
|
||||
'py.test',
|
||||
'--quiet',
|
||||
'tests',
|
||||
*session.posargs
|
||||
)
|
138
pybadges/__init__.py
Normal file
@@ -0,0 +1,138 @@
|
||||
# Copyright 2018 The pybadge Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Creates a github-style badge as a SVG image.
|
||||
|
||||
This package seeks to generate semantically-identical output to the JavaScript
|
||||
gh-badges library
|
||||
(https://github.com/badges/shields/blob/master/doc/gh-badges.md)
|
||||
|
||||
>>> badge(left_text='coverage', right_text='23%', right_color='red')
|
||||
'<svg...</svg>'
|
||||
>>> badge(left_text='build', right_text='green', right_color='green',
|
||||
... whole_link="http://www.example.com/")
|
||||
'<svg...</svg>'
|
||||
>>> # base64-encoded PNG image
|
||||
>>> image_data = 'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAD0lEQVQI12P4zwAD/xkYAA/+Af8iHnLUAAAAAElFTkSuQmCC'
|
||||
>>> badge(left_text='build', right_text='green', right_color='green',
|
||||
... logo="data:image/png;base64," + image_data)
|
||||
'<svg...</svg>'
|
||||
"""
|
||||
|
||||
import jinja2
|
||||
from typing import Optional
|
||||
from xml.dom import minidom
|
||||
|
||||
from pybadges import text_measurer
|
||||
from pybadges import precalculated_text_measurer
|
||||
|
||||
|
||||
_JINJA2_ENVIRONMENT = jinja2.Environment(
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
loader=jinja2.PackageLoader('pybadges', '.'),
|
||||
autoescape=jinja2.select_autoescape(['svg']))
|
||||
|
||||
# Use the same color scheme as describe in:
|
||||
# https://github.com/badges/shields/blob/master/lib/colorscheme.json
|
||||
|
||||
_NAME_TO_COLOR = {
|
||||
'brightgreen': '#4c1',
|
||||
'green': '#97CA00',
|
||||
'yellow': '#dfb317',
|
||||
'yellowgreen': '#a4a61d',
|
||||
'orange': '#fe7d37',
|
||||
'red': '#e05d44',
|
||||
'blue': '#007ec6',
|
||||
'grey': '#555',
|
||||
'gray': '#555',
|
||||
'lightgrey': '#9f9f9f',
|
||||
'lightgray': '#9f9f9f',
|
||||
}
|
||||
|
||||
|
||||
def _remove_blanks(node):
|
||||
for x in node.childNodes:
|
||||
if x.nodeType == minidom.Node.TEXT_NODE:
|
||||
if x.nodeValue:
|
||||
x.nodeValue = x.nodeValue.strip()
|
||||
elif x.nodeType == minidom.Node.ELEMENT_NODE:
|
||||
_remove_blanks(x)
|
||||
|
||||
|
||||
def badge(left_text: str, right_text: str, left_link: Optional[str] = None,
|
||||
right_link: Optional[str] = None,
|
||||
whole_link: Optional[str] = None, logo: Optional[str] = None,
|
||||
left_color: str = '#555', right_color: str = '#007ec6',
|
||||
measurer: Optional[text_measurer.TextMeasurer] = None) -> str:
|
||||
"""Creates a github-style badge as an SVG image.
|
||||
|
||||
>>> badge(left_text='coverage', right_text='23%', right_color='red')
|
||||
'<svg...</svg>'
|
||||
>>> badge(left_text='build', right_text='green', right_color='green',
|
||||
... whole_link="http://www.example.com/")
|
||||
'<svg...</svg>'
|
||||
|
||||
Args:
|
||||
left_text: The text that should appear on the left-hand-side of the
|
||||
badge e.g. "coverage".
|
||||
right_text: The text that should appear on the right-hand-side of the
|
||||
badge e.g. "23%".
|
||||
left_link: The URL that should be redirected to when the left-hand text
|
||||
is selected.
|
||||
right_link: The URL that should be redirected to when the right-hand
|
||||
text is selected.
|
||||
whole_link: The link that should be redirected to when the badge is
|
||||
selected. If set then left_link and right_right may not be set.
|
||||
logo: A url representing a logo that will be displayed inside the
|
||||
badge. Can be a data URL e.g. "data:image/svg+xml;utf8,<svg..."
|
||||
left_color: The color of the part of the badge containing the left-hand
|
||||
text. Can be an valid CSS color
|
||||
(see https://developer.mozilla.org/en-US/docs/Web/CSS/color) or a
|
||||
color name defined here:
|
||||
https://github.com/badges/shields/blob/master/lib/colorscheme.json
|
||||
right_color: The color of the part of the badge containing the
|
||||
right-hand text. Can be an valid CSS color
|
||||
(see https://developer.mozilla.org/en-US/docs/Web/CSS/color) or a
|
||||
color name defined here:
|
||||
https://github.com/badges/shields/blob/master/lib/colorscheme.json
|
||||
measurer: A text_measurer.TextMeasurer that can be used to measure the
|
||||
width of left_text and right_text.
|
||||
|
||||
"""
|
||||
if measurer is None:
|
||||
measurer = (
|
||||
precalculated_text_measurer.PrecalculatedTextMeasurer
|
||||
.default())
|
||||
|
||||
if (left_link or right_link) and whole_link:
|
||||
raise ValueError(
|
||||
'whole_link may not bet set with left_link or right_link')
|
||||
template = _JINJA2_ENVIRONMENT.get_template('badge-template-full.svg')
|
||||
svg = template.render(
|
||||
left_text=left_text,
|
||||
right_text=right_text,
|
||||
left_text_width=measurer.text_width(left_text) / 10.0,
|
||||
right_text_width=measurer.text_width(right_text) / 10.0,
|
||||
left_link=left_link,
|
||||
right_link=right_link,
|
||||
whole_link=whole_link,
|
||||
logo=logo,
|
||||
left_color=_NAME_TO_COLOR.get(left_color, left_color),
|
||||
right_color=_NAME_TO_COLOR.get(right_color, right_color),
|
||||
)
|
||||
xml = minidom.parseString(svg)
|
||||
_remove_blanks(xml)
|
||||
xml.normalize()
|
||||
return xml.documentElement.toxml()
|
124
pybadges/__main__.py
Normal file
@@ -0,0 +1,124 @@
|
||||
# Copyright 2018 The pybadge Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Output a github-style badge as an SVG image given some text and colors.
|
||||
|
||||
For more information, run:
|
||||
$ python3 -m pybadges --help
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import tempfile
|
||||
import webbrowser
|
||||
|
||||
import pybadges
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='generate a github-style badge given some text and colors')
|
||||
|
||||
parser.add_argument(
|
||||
'--left-text',
|
||||
default='license',
|
||||
help='the text to show on the left-hand-side of the badge')
|
||||
parser.add_argument(
|
||||
'--right-text',
|
||||
default='APACHE',
|
||||
help='the text to show on the right-hand-side of the badge')
|
||||
parser.add_argument(
|
||||
'--whole-link',
|
||||
default=None,
|
||||
help='the url to redirect to when the badge is clicked')
|
||||
parser.add_argument(
|
||||
'--left-link',
|
||||
default=None,
|
||||
help='the url to redirect to when the left-hand of the badge is ' +
|
||||
'clicked')
|
||||
parser.add_argument(
|
||||
'--right-link',
|
||||
default=None,
|
||||
help='the url to redirect to when the right-hand of the badge is ' +
|
||||
'clicked')
|
||||
parser.add_argument(
|
||||
'--left-color',
|
||||
default='#555',
|
||||
help='the background color of the left-hand-side of the badge')
|
||||
parser.add_argument(
|
||||
'--right-color',
|
||||
default='#007ec6',
|
||||
help='the background color of the right-hand-side of the badge')
|
||||
parser.add_argument(
|
||||
'--logo',
|
||||
default=None,
|
||||
help='a URI reference to a logo to display in the badge')
|
||||
parser.add_argument(
|
||||
'--browser',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='display the badge in a browser tab')
|
||||
parser.add_argument(
|
||||
'--use-pil-text-measurer',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='use the PilMeasurer to measure the length of text (kerning may '
|
||||
'be more precise for non-Western languages. ' +
|
||||
'--deja-vu-sans-path must also be set.')
|
||||
parser.add_argument(
|
||||
'--deja-vu-sans-path',
|
||||
default=None,
|
||||
help='the path to the ttf font file containing DejaVu Sans. If not ' +
|
||||
'present on your system, you can download it from ' +
|
||||
'https://www.fontsquirrel.com/fonts/dejavu-sans')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if (args.left_link or args.right_link) and args.whole_link:
|
||||
print(
|
||||
'argument --whole-link: cannot be set with ' +
|
||||
'--left-link or --right-link',
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
measurer = None
|
||||
if args.use_pil_text_measurer:
|
||||
if args.deja_vu_sans_path is None:
|
||||
print(
|
||||
'argument --use-pil-text-measurer: must also set ' +
|
||||
'--deja-vu-sans-path',
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
from pybadges import pil_text_measurer
|
||||
measurer = pil_text_measurer.PilMeasurer(args.deja_vu_sans_path)
|
||||
|
||||
badge = pybadges.badge(left_text=args.left_text, right_text=args.right_text,
|
||||
left_link=args.left_link, right_link=args.right_link,
|
||||
whole_link=args.whole_link,
|
||||
left_color=args.left_color,
|
||||
right_color=args.right_color,
|
||||
logo=args.logo,
|
||||
measurer=measurer)
|
||||
|
||||
if args.browser:
|
||||
_, badge_path = tempfile.mkstemp(suffix='.svg')
|
||||
with open(badge_path, 'w') as f:
|
||||
f.write(badge)
|
||||
|
||||
webbrowser.open_new_tab('file://' + badge_path)
|
||||
else:
|
||||
print(badge, end='')
|
||||
|
||||
|
||||
main()
|
41
pybadges/badge-template-full.svg
Normal file
@@ -0,0 +1,41 @@
|
||||
{% set logo_width = 14 if logo else 0 %}
|
||||
{% set logo_padding = 3 if (logo and left_text) else 0 %}
|
||||
{% set left_width = left_text_width + 10 + logo_width + logo_padding %}
|
||||
{% set right_width = right_text_width + 10 %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{ left_width + right_width }}" height="20">
|
||||
<linearGradient id="smooth" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
|
||||
<clipPath id="round">
|
||||
<rect width="{{ left_width + right_width }}" height="20" rx="3" fill="#fff"/>
|
||||
</clipPath>
|
||||
|
||||
<g clip-path="url(#round)">
|
||||
<rect width="{{ left_width }}" height="20" fill="{{ left_color }}"/>
|
||||
<rect x="{{ left_width }}" width="{{ right_width }}" height="20" fill="{{ right_color }}"/>
|
||||
<rect width="{{ left_width + right_width }}" height="20" fill="url(#smooth)"/>
|
||||
</g>
|
||||
|
||||
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110">
|
||||
{% if logo %}
|
||||
<image x="5" y="3" width="{{ logo_width}}" height="14" xlink:href="{{ logo}}"/>
|
||||
{% endif %}
|
||||
<text x="{{ (((left_width+logo_width+logo_padding)/2)+1)*10 }}" y="150" fill="#010101" fill-opacity=".3" transform="scale(0.1)" textLength="{{ (left_width-(10+logo_width+logo_padding))*10 }}" lengthAdjust="spacing">{{ left_text }}</text>
|
||||
<text x="{{ (((left_width+logo_width+logo_padding)/2)+1)*10 }}" y="140" transform="scale(0.1)" textLength="{{ (left_width-(10+logo_width+logo_padding))*10 }}" lengthAdjust="spacing">{{ left_text }}</text>
|
||||
<text x="{{ (left_width+right_width/2-1)*10 }}" y="150" fill="#010101" fill-opacity=".3" transform="scale(0.1)" textLength="{{ (right_width-10)*10 }}" lengthAdjust="spacing">{{ right_text }}</text>
|
||||
<text x="{{ (left_width+right_width/2-1)*10 }}" y="140" transform="scale(0.1)" textLength="{{ (right_width-10)*10 }}" lengthAdjust="spacing">{{ right_text}}</text>
|
||||
|
||||
{% if left_link or whole_link %}
|
||||
<a xlink:href="{{ left_link or wholelink }}">
|
||||
<rect width="{{ left_width }}" height="20" fill="rgba(0,0,0,0)"/>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if right_link or whole_link %}
|
||||
<a xlink:href="{{ right_link or whole_link }}">
|
||||
<rect x="{{ left_width }}" width="{{ right_width }}" height="20" fill="rgba(0,0,0,0)"/>
|
||||
</a>
|
||||
{% endif %}
|
||||
</g>
|
||||
</svg>
|
BIN
pybadges/default-widths.json.xz
Normal file
40
pybadges/pil_text_measurer.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# Copyright 2018 The pybadge Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Measure the width, in pixels, of a string rendered using DejaVu Sans 110pt.
|
||||
|
||||
Uses a PIL/Pillow to determine the string length.
|
||||
"""
|
||||
|
||||
from PIL import ImageFont
|
||||
|
||||
from pybadges import text_measurer
|
||||
|
||||
|
||||
class PilMeasurer(text_measurer.TextMeasurer):
|
||||
"""Measures the width of a string using PIL/Pillow."""
|
||||
|
||||
def __init__(self, deja_vu_sans_path: str):
|
||||
"""Initializer for PilMeasurer.
|
||||
|
||||
Args:
|
||||
deja_vu_sans_path: The path to the DejaVu Sans TrueType (.ttf) font
|
||||
file.
|
||||
"""
|
||||
self._font = ImageFont.truetype(deja_vu_sans_path, 110)
|
||||
|
||||
def text_width(self, text: str) -> float:
|
||||
"""Returns the width, in pixels, of a string in DejaVu Sans 110pt."""
|
||||
width, _ = self._font.getsize(text)
|
||||
return width
|
204
pybadges/precalculate_text.py
Normal file
@@ -0,0 +1,204 @@
|
||||
# Copyright 2018 The pybadge Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Creates a JSON file that can be used by precalculated_text_measurer.py.
|
||||
|
||||
Creates a JSON file that can be used by
|
||||
precalculated_test_measurer.PrecalculatedTextMeasurer to calculate the pixel
|
||||
length of text strings rendered in DejaVu Sans font.
|
||||
|
||||
The output JSON object is formatted like:
|
||||
{
|
||||
'mean-character-length': <float: the average pixel length of a character in
|
||||
DejaVu Sans at 110pt>,
|
||||
'character-lengths': <Map[str, float]: a mapping between single characters
|
||||
and their length in DejaVu Sans at 110pt>,
|
||||
'kerning-characters': <str: a string containing all of the characters that
|
||||
kerning information is available for>,
|
||||
'kerning-pairs': <Map[str, float]: a mapping between pairs of characters
|
||||
(e.g. "IJ") to the amount of kerning distance between
|
||||
them. Positive values are *subtracted* from the string
|
||||
length e.g. text-length-of("IJ") =>
|
||||
(character-lengths["I"] + character-lengths["J"]
|
||||
- kerning-pairs["IJ"])
|
||||
If two characters both appear in 'kerning-characters' but
|
||||
don't have an entry in 'kerning-pairs' then the kerning
|
||||
distance between them is zero.
|
||||
}
|
||||
|
||||
For information about the commands, run:
|
||||
$ python3 - m pybadges.precalculate_text --help
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import itertools
|
||||
import json
|
||||
import os.path
|
||||
import statistics
|
||||
from typing import Iterable, Mapping, TextIO
|
||||
|
||||
from fontTools import ttLib
|
||||
|
||||
from pybadges import pil_text_measurer
|
||||
from pybadges import text_measurer
|
||||
|
||||
|
||||
def generate_supported_characters(deja_vu_sans_path: str) -> Iterable[str]:
|
||||
"""Generate the characters support by the font at the given path."""
|
||||
font = ttLib.TTFont(deja_vu_sans_path)
|
||||
for cmap in font['cmap'].tables:
|
||||
if cmap.isUnicode():
|
||||
for code in cmap.cmap:
|
||||
yield chr(code)
|
||||
|
||||
|
||||
def generate_encodeable_characters(characters: Iterable[str],
|
||||
encodings: Iterable[str]) -> Iterable[str]:
|
||||
"""Generates the subset of 'characters' that can be encoded by 'encodings'.
|
||||
|
||||
Args:
|
||||
characters: The characters to check for encodeability e.g. 'abcd'.
|
||||
encodings: The encodings to check against e.g. ['cp1252', 'iso-8859-5'].
|
||||
|
||||
Returns:
|
||||
The subset of 'characters' that can be encoded using one of the provided
|
||||
encodings.
|
||||
"""
|
||||
for c in characters:
|
||||
for encoding in encodings:
|
||||
try:
|
||||
c.encode(encoding)
|
||||
yield c
|
||||
except UnicodeEncodeError:
|
||||
pass
|
||||
|
||||
|
||||
def calculate_character_to_length_mapping(
|
||||
measurer: text_measurer.TextMeasurer,
|
||||
characters: Iterable[str]) -> Mapping[str, float]:
|
||||
"""Return a mapping between each given character and its length.
|
||||
|
||||
Args:
|
||||
measurer: The TextMeasurer used to measure the width of the text in
|
||||
pixels.
|
||||
characters: The characters to measure e.g. "ml".
|
||||
|
||||
Returns:
|
||||
A mapping from the given characters to their length in pixels, as
|
||||
determined by 'measurer' e.g. {'m': 5.2, 'l', 1.2}.
|
||||
"""
|
||||
char_to_length = {}
|
||||
|
||||
for c in characters:
|
||||
char_to_length[c] = measurer.text_width(c)
|
||||
return char_to_length
|
||||
|
||||
|
||||
def calculate_pair_to_kern_mapping(
|
||||
measurer: text_measurer.TextMeasurer,
|
||||
char_to_length: Mapping[str, float],
|
||||
characters: Iterable[str]) -> Mapping[str, float]:
|
||||
"""Returns a mapping between each *pair* of characters and their kerning.
|
||||
|
||||
Args:
|
||||
measurer: The TextMeasurer used to measure the width of each pair of
|
||||
characters.
|
||||
char_to_length: A mapping between characters and their length in pixels.
|
||||
Must contain every character in 'characters' e.g.
|
||||
{'h': 5.2, 'e': 4.0, 'l', 1.2, 'o': 5.0}.
|
||||
characters: The characters to generate the kerning mapping for e.g.
|
||||
'hel'.
|
||||
|
||||
Returns:
|
||||
A mapping between each pair of given characters
|
||||
(e.g. 'hh', he', hl', 'eh', 'ee', 'el', 'lh, 'le', 'll') and the kerning
|
||||
adjustment for that pair of characters i.e. the difference between the
|
||||
length of the two characters calculated using 'char_to_length' vs.
|
||||
the length calculated by `measurer`. Positive values indicate that the
|
||||
length is less than using the sum of 'char_to_length'. Zero values are
|
||||
excluded from the map e.g. {'hl': 3.1, 'ee': -0.5}.
|
||||
"""
|
||||
pair_to_kerning = {}
|
||||
for a, b in itertools.permutations(characters, 2):
|
||||
kerned_width = measurer.text_width(a + b)
|
||||
unkerned_width = char_to_length[a] + char_to_length[b]
|
||||
kerning = unkerned_width - kerned_width
|
||||
if abs(kerning) > 0.05:
|
||||
pair_to_kerning[a + b] = round(kerning, 3)
|
||||
return pair_to_kerning
|
||||
|
||||
|
||||
def write_json(f: TextIO, deja_vu_sans_path: str,
|
||||
measurer: text_measurer.TextMeasurer,
|
||||
encodings: Iterable[str]) -> None:
|
||||
"""Write the data required by PrecalculatedTextMeasurer to a stream."""
|
||||
supported_characters = list(
|
||||
generate_supported_characters(deja_vu_sans_path))
|
||||
kerning_characters = ''.join(
|
||||
generate_encodeable_characters(supported_characters, encodings))
|
||||
char_to_length = calculate_character_to_length_mapping(measurer,
|
||||
supported_characters)
|
||||
pair_to_kerning = calculate_pair_to_kern_mapping(measurer, char_to_length,
|
||||
kerning_characters)
|
||||
json.dump(
|
||||
{'mean-character-length': statistics.mean(char_to_length.values()),
|
||||
'character-lengths': char_to_length,
|
||||
'kerning-characters': kerning_characters,
|
||||
'kerning-pairs': pair_to_kerning},
|
||||
f, sort_keys=True, indent=1)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='generate a github-style badge given some text and colors')
|
||||
|
||||
parser.add_argument(
|
||||
'--deja-vu-sans-path',
|
||||
required=True,
|
||||
help='the path to the ttf font file containing DejaVu Sans. If not ' +
|
||||
'present on your system, you can download it from ' +
|
||||
'https://www.fontsquirrel.com/fonts/dejavu-sans')
|
||||
|
||||
parser.add_argument(
|
||||
'--kerning-pair-encodings',
|
||||
action='append',
|
||||
default=['cp1252'],
|
||||
help='only include kerning pairs for the given encodings')
|
||||
|
||||
parser.add_argument(
|
||||
'--output-json-file',
|
||||
default=os.path.join(os.path.dirname(__file__),
|
||||
'default-widths.json.xz'),
|
||||
help='the path where the generated JSON will be placed. If the ' +
|
||||
'provided filename extension ends with .xz then the output' +
|
||||
'will be compressed using lzma.')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
measurer = pil_text_measurer.PilMeasurer(args.deja_vu_sans_path)
|
||||
|
||||
def create_file():
|
||||
if args.output_json_file.endswith('.xz'):
|
||||
import lzma
|
||||
return lzma.open(args.output_json_file, 'wt')
|
||||
else:
|
||||
return open(args.output_json_file, 'wt')
|
||||
|
||||
with create_file() as f:
|
||||
write_json(
|
||||
f, args.deja_vu_sans_path, measurer, args.kerning_pair_encodings)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
92
pybadges/precalculated_text_measurer.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# Copyright 2018 The pybadge Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Measure the width, in pixels, of a string rendered using DejaVu Sans 110pt.
|
||||
|
||||
Uses a precalculated set of metrics to calculate the string length.
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
import pkg_resources
|
||||
from typing import Mapping, TextIO, Type
|
||||
|
||||
from pybadges import text_measurer
|
||||
|
||||
|
||||
class PrecalculatedTextMeasurer(text_measurer.TextMeasurer):
|
||||
"""Measures the width of a string using a precalculated set of tables."""
|
||||
|
||||
_default_cache = None
|
||||
|
||||
def __init__(self, default_character_width: float,
|
||||
char_to_width: Mapping[str, float],
|
||||
pair_to_kern: Mapping[str, float]):
|
||||
"""Initializer for PrecalculatedTextMeasurer.
|
||||
|
||||
Args:
|
||||
default_character_width: the average width, in pixels, of a
|
||||
character in DejaVu Sans 110pt.
|
||||
char_to_width: a mapping between a character and it's width,
|
||||
in pixels, in DejaVu Sans 110pt.
|
||||
pair_to_kern: a mapping between pairs of characters and the kerning
|
||||
distance between them e.g. text_width("IJ") =>
|
||||
(char_to_width["I"] + char_to_width["J"]
|
||||
- pair_to_kern.get("IJ", 0))
|
||||
"""
|
||||
self._default_character_width = default_character_width
|
||||
self._char_to_width = char_to_width
|
||||
self._pair_to_kern = pair_to_kern
|
||||
|
||||
def text_width(self, text: str) -> float:
|
||||
"""Returns the width, in pixels, of a string in DejaVu Sans 110pt."""
|
||||
width = 0
|
||||
for index, c in enumerate(text):
|
||||
width += self._char_to_width.get(c, self._default_character_width)
|
||||
width -= self._pair_to_kern.get(text[index:index + 2], 0)
|
||||
|
||||
return width
|
||||
|
||||
@staticmethod
|
||||
def from_json(f: TextIO) -> 'PrecalculatedTextMeasurer':
|
||||
"""Return a PrecalculatedTextMeasurer given a JSON stream.
|
||||
|
||||
See precalculate_text.py for details on the required format.
|
||||
"""
|
||||
o = json.load(f)
|
||||
return PrecalculatedTextMeasurer(o['mean-character-length'],
|
||||
o['character-lengths'],
|
||||
o['kerning-pairs'])
|
||||
|
||||
@classmethod
|
||||
def default(cls) -> 'PrecalculatedTextMeasurer':
|
||||
"""Returns a reasonable default PrecalculatedTextMeasurer."""
|
||||
if cls._default_cache is not None:
|
||||
return cls._default_cache
|
||||
|
||||
if pkg_resources.resource_exists(__name__, 'default-widths.json.xz'):
|
||||
import lzma
|
||||
with pkg_resources.resource_stream(__name__,
|
||||
'default-widths.json.xz') as f:
|
||||
with lzma.open(f, "rt") as g:
|
||||
cls._default_cache = PrecalculatedTextMeasurer.from_json(g)
|
||||
return cls._default_cache
|
||||
elif pkg_resources.resource_exists(__name__, 'default-widths.json'):
|
||||
with pkg_resources.resource_stream(__name__,
|
||||
'default-widths.json') as f:
|
||||
cls._default_cache = PrecalculatedTextMeasurer.from_json(
|
||||
io.TextIOWrapper(f, encoding='utf-8'))
|
||||
return cls._default_cache
|
||||
else:
|
||||
raise ValueError('could not load default-widths.json')
|
26
pybadges/text_measurer.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Copyright 2018 The pybadge Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Measure the width, in pixels, of a string rendered using DejaVu Sans 110pt.
|
||||
|
||||
Contains only an abstract base class.
|
||||
"""
|
||||
|
||||
|
||||
class TextMeasurer:
|
||||
"""The abstract base class for text measuring classes."""
|
||||
|
||||
def text_width(self, text: str) -> float:
|
||||
"""Returns the width, in pixels, of a string in DejaVu Sans 110pt."""
|
||||
raise NotImplementedError('text_width not implemented')
|
51
setup.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# Copyright 2018 The pybadge Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""A setup module for pybadges."""
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='pybadges',
|
||||
version='0.0.1',
|
||||
author='Brian Quinlan',
|
||||
author_email='brian@sweetapp.com',
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Operating System :: OS Independent',
|
||||
],
|
||||
description='A library and command-line tool for generating Github-style ' +
|
||||
'badges',
|
||||
include_package_data=True,
|
||||
keywords="github gh-badges badge shield status",
|
||||
package_data={'pybadges': ['badge-template-full.svg',
|
||||
'default-widths.json.xz']},
|
||||
long_description="test",
|
||||
install_requires=['Jinja2>=2'],
|
||||
extras_require={
|
||||
'pil-measurement': ['Pillow>=5'],
|
||||
'dev': ['fonttools>=3.26', 'nox-automation>=0.19', 'Pillow>=5',
|
||||
'pytest>=3.6'],
|
||||
},
|
||||
license='Apache-2.0',
|
||||
packages=["pybadges"],
|
||||
url='https://github.com/brianquinlan/cloud-opensource-python/pybadges')
|
1
tests/golden-images/accuracy.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="20" width="110.9" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect fill="#fff" height="20" rx="3" width="110.9"/></clipPath><g clip-path="url(#round)"><rect fill="#555" height="20" width="76.4"/><rect fill="#dfb317" height="20" width="34.5" x="76.4"/><rect fill="url(#smooth)" height="20" width="110.9"/></g><g fill="#fff" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110" text-anchor="middle"><image height="14" width="14" x="5" xlink:href="data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="120" width="120"><circle cx="60" cy="60" r="60" fill="#c00" /><circle cx="60" cy="60" r="50" fill="#ddd" /><circle cx="60" cy="60" r="40" fill="#c00" /><circle cx="60" cy="60" r="30" fill="#ddd" /><circle cx="60" cy="60" r="20" fill="#c00" /></svg>" y="3"/><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="494.00000000000006" transform="scale(0.1)" x="477.0" y="150">accuracy</text><text lengthAdjust="spacing" textLength="494.00000000000006" transform="scale(0.1)" x="477.0" y="140">accuracy</text><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="245.0" transform="scale(0.1)" x="926.5" y="150">70%</text><text lengthAdjust="spacing" textLength="245.0" transform="scale(0.1)" x="926.5" y="140">70%</text></g></svg>
|
After Width: | Height: | Size: 1.8 KiB |
1
tests/golden-images/build-failure.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="20" width="82.30000000000001" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect fill="#fff" height="20" rx="3" width="82.30000000000001"/></clipPath><g clip-path="url(#round)"><rect fill="#555" height="20" width="37.2"/><rect fill="#c00" height="20" width="45.1" x="37.2"/><rect fill="url(#smooth)" height="20" width="82.30000000000001"/></g><g fill="#fff" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110" text-anchor="middle"><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="272.0" transform="scale(0.1)" x="196.0" y="150">build</text><text lengthAdjust="spacing" textLength="272.0" transform="scale(0.1)" x="196.0" y="140">build</text><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="351.0" transform="scale(0.1)" x="587.5" y="150">failure</text><text lengthAdjust="spacing" textLength="351.0" transform="scale(0.1)" x="587.5" y="140">failure</text></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
1
tests/golden-images/build-passing.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="20" width="89.4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect fill="#fff" height="20" rx="3" width="89.4"/></clipPath><g clip-path="url(#round)"><rect fill="#555" height="20" width="37.2"/><rect fill="#97CA00" height="20" width="52.2" x="37.2"/><rect fill="url(#smooth)" height="20" width="89.4"/></g><g fill="#fff" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110" text-anchor="middle"><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="272.0" transform="scale(0.1)" x="196.0" y="150">build</text><text lengthAdjust="spacing" textLength="272.0" transform="scale(0.1)" x="196.0" y="140">build</text><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="422.0" transform="scale(0.1)" x="623.0" y="150">passing</text><text lengthAdjust="spacing" textLength="422.0" transform="scale(0.1)" x="623.0" y="140">passing</text></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
1
tests/golden-images/build-running.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="20" width="89.80000000000001" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect fill="#fff" height="20" rx="3" width="89.80000000000001"/></clipPath><g clip-path="url(#round)"><rect fill="#555" height="20" width="37.2"/><rect fill="#007ec6" height="20" width="52.6" x="37.2"/><rect fill="url(#smooth)" height="20" width="89.80000000000001"/></g><g fill="#fff" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110" text-anchor="middle"><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="272.0" transform="scale(0.1)" x="196.0" y="150">build</text><text lengthAdjust="spacing" textLength="272.0" transform="scale(0.1)" x="196.0" y="140">build</text><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="426.0" transform="scale(0.1)" x="625.0" y="150">running</text><text lengthAdjust="spacing" textLength="426.0" transform="scale(0.1)" x="625.0" y="140">running</text></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
1
tests/golden-images/complete.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="20" width="136.0" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect fill="#fff" height="20" rx="3" width="136.0"/></clipPath><g clip-path="url(#round)"><rect fill="#97CA00" height="20" width="78.4"/><rect fill="#fb3" height="20" width="57.6" x="78.4"/><rect fill="url(#smooth)" height="20" width="136.0"/></g><g fill="#fff" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110" text-anchor="middle"><image height="14" width="14" x="5" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAD0lEQVQI12P4zwAD/xkYAA/+Af8iHnLUAAAAAElFTkSuQmCC" y="3"/><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="514.0" transform="scale(0.1)" x="487.0" y="150">complete</text><text lengthAdjust="spacing" textLength="514.0" transform="scale(0.1)" x="487.0" y="140">complete</text><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="476.0" transform="scale(0.1)" x="1062.0" y="150">example</text><text lengthAdjust="spacing" textLength="476.0" transform="scale(0.1)" x="1062.0" y="140">example</text><a xlink:href="http://www.complete.com/"><rect fill="rgba(0,0,0,0)" height="20" width="78.4"/></a><a xlink:href="http://www.example.com"><rect fill="rgba(0,0,0,0)" height="20" width="57.6" x="78.4"/></a></g></svg>
|
After Width: | Height: | Size: 1.5 KiB |
1
tests/golden-images/github.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="20" width="109.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect fill="#fff" height="20" rx="3" width="109.1"/></clipPath><g clip-path="url(#round)"><rect fill="#555" height="20" width="45.4"/><rect fill="#007ec6" height="20" width="63.7" x="45.4"/><rect fill="url(#smooth)" height="20" width="109.1"/></g><g fill="#fff" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110" text-anchor="middle"><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="354.0" transform="scale(0.1)" x="237.0" y="150">github</text><text lengthAdjust="spacing" textLength="354.0" transform="scale(0.1)" x="237.0" y="140">github</text><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="537.0" transform="scale(0.1)" x="762.5" y="150">pybadges</text><text lengthAdjust="spacing" textLength="537.0" transform="scale(0.1)" x="762.5" y="140">pybadges</text><a xlink:href=""><rect fill="rgba(0,0,0,0)" height="20" width="45.4"/></a><a xlink:href="TODO"><rect fill="rgba(0,0,0,0)" height="20" width="63.7" x="45.4"/></a></g></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
tests/golden-images/license.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="20" width="124.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect fill="#fff" height="20" rx="3" width="124.1"/></clipPath><g clip-path="url(#round)"><rect fill="#555" height="20" width="48.5"/><rect fill="#007ec6" height="20" width="75.6" x="48.5"/><rect fill="url(#smooth)" height="20" width="124.1"/></g><g fill="#fff" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110" text-anchor="middle"><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="385.0" transform="scale(0.1)" x="252.5" y="150">license</text><text lengthAdjust="spacing" textLength="385.0" transform="scale(0.1)" x="252.5" y="140">license</text><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="656.0" transform="scale(0.1)" x="853.0" y="150">APACHE 2.0</text><text lengthAdjust="spacing" textLength="656.0" transform="scale(0.1)" x="853.0" y="140">APACHE 2.0</text><a xlink:href="https://opensource.org/licenses"><rect fill="rgba(0,0,0,0)" height="20" width="48.5"/></a><a xlink:href="https://opensource.org/licenses/Apache-2.0"><rect fill="rgba(0,0,0,0)" height="20" width="75.6" x="48.5"/></a></g></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
tests/golden-images/pip.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="20" width="127.30000000000001" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect fill="#fff" height="20" rx="3" width="127.30000000000001"/></clipPath><g clip-path="url(#round)"><rect fill="#555" height="20" width="63.6"/><rect fill="#007ec6" height="20" width="63.7" x="63.6"/><rect fill="url(#smooth)" height="20" width="127.30000000000001"/></g><g fill="#fff" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110" text-anchor="middle"><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="536.0" transform="scale(0.1)" x="328.0" y="150">pip install</text><text lengthAdjust="spacing" textLength="536.0" transform="scale(0.1)" x="328.0" y="140">pip install</text><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="537.0" transform="scale(0.1)" x="944.5" y="150">pybadges</text><text lengthAdjust="spacing" textLength="537.0" transform="scale(0.1)" x="944.5" y="140">pybadges</text><a xlink:href="https://pip.pypa.io/en/stable/installing/"><rect fill="rgba(0,0,0,0)" height="20" width="63.6"/></a><a xlink:href="https://pypi.org/project/pybadges/"><rect fill="rgba(0,0,0,0)" height="20" width="63.7" x="63.6"/></a></g></svg>
|
After Width: | Height: | Size: 1.4 KiB |
1
tests/golden-images/python.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="20" width="191.0" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect fill="#fff" height="20" rx="3" width="191.0"/></clipPath><g clip-path="url(#round)"><rect fill="#555" height="20" width="65.5"/><rect fill="#007ec6" height="20" width="125.5" x="65.5"/><rect fill="url(#smooth)" height="20" width="191.0"/></g><g fill="#fff" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110" text-anchor="middle"><image height="14" width="14" x="5" xlink:href="https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/python.svg" y="3"/><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="385.0" transform="scale(0.1)" x="422.5" y="150">python</text><text lengthAdjust="spacing" textLength="385.0" transform="scale(0.1)" x="422.5" y="140">python</text><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="1155.0" transform="scale(0.1)" x="1272.5" y="150">3.2, 3.3, 3.4, 3.5, 3.6</text><text lengthAdjust="spacing" textLength="1155.0" transform="scale(0.1)" x="1272.5" y="140">3.2, 3.3, 3.4, 3.5, 3.6</text><a xlink:href=""><rect fill="rgba(0,0,0,0)" height="20" width="65.5"/></a><a xlink:href="https://www.python.org/"><rect fill="rgba(0,0,0,0)" height="20" width="125.5" x="65.5"/></a></g></svg>
|
After Width: | Height: | Size: 1.4 KiB |
1
tests/golden-images/saying-arabic.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="20" width="306.4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect fill="#fff" height="20" rx="3" width="306.4"/></clipPath><g clip-path="url(#round)"><rect fill="#555" height="20" width="46.0"/><rect fill="#007ec6" height="20" width="260.4" x="46.0"/><rect fill="url(#smooth)" height="20" width="306.4"/></g><g fill="#fff" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110" text-anchor="middle"><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="360.0" transform="scale(0.1)" x="240.0" y="150">saying</text><text lengthAdjust="spacing" textLength="360.0" transform="scale(0.1)" x="240.0" y="140">saying</text><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="2504.0" transform="scale(0.1)" x="1752.0" y="150">أباد الله خضراءهم ابذل لصديقك دمك ومالك</text><text lengthAdjust="spacing" textLength="2504.0" transform="scale(0.1)" x="1752.0" y="140">أباد الله خضراءهم ابذل لصديقك دمك ومالك</text></g></svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
tests/golden-images/saying-chinese.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="20" width="334.90003386386746" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect fill="#fff" height="20" rx="3" width="334.90003386386746"/></clipPath><g clip-path="url(#round)"><rect fill="#555" height="20" width="46.0"/><rect fill="#007ec6" height="20" width="288.90003386386746" x="46.0"/><rect fill="url(#smooth)" height="20" width="334.90003386386746"/></g><g fill="#fff" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110" text-anchor="middle"><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="360.0" transform="scale(0.1)" x="240.0" y="150">saying</text><text lengthAdjust="spacing" textLength="360.0" transform="scale(0.1)" x="240.0" y="140">saying</text><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="2789.0003386386747" transform="scale(0.1)" x="1894.5001693193374" y="150">不聞不若聞之,聞之不若見之,見之不若知之,知之不若行之;學至於行之而止矣</text><text lengthAdjust="spacing" textLength="2789.0003386386747" transform="scale(0.1)" x="1894.5001693193374" y="140">不聞不若聞之,聞之不若見之,見之不若知之,知之不若行之;學至於行之而止矣</text></g></svg>
|
After Width: | Height: | Size: 1.4 KiB |
1
tests/golden-images/saying-russian.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="20" width="319.7" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect fill="#fff" height="20" rx="3" width="319.7"/></clipPath><g clip-path="url(#round)"><rect fill="#555" height="20" width="46.0"/><rect fill="#007ec6" height="20" width="273.7" x="46.0"/><rect fill="url(#smooth)" height="20" width="319.7"/></g><g fill="#fff" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110" text-anchor="middle"><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="360.0" transform="scale(0.1)" x="240.0" y="150">saying</text><text lengthAdjust="spacing" textLength="360.0" transform="scale(0.1)" x="240.0" y="140">saying</text><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="2637.0" transform="scale(0.1)" x="1818.5" y="150">Без труда́ не вы́тащишь и ры́бку из пруда́.</text><text lengthAdjust="spacing" textLength="2637.0" transform="scale(0.1)" x="1818.5" y="140">Без труда́ не вы́тащишь и ры́бку из пруда́.</text></g></svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
tests/golden-images/status.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="20" width="65.2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect fill="#fff" height="20" rx="3" width="65.2"/></clipPath><g clip-path="url(#round)"><rect fill="#555" height="20" width="43.7"/><rect fill="#007ec6" height="20" width="21.5" x="43.7"/><rect fill="url(#smooth)" height="20" width="65.2"/></g><g fill="#fff" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110" text-anchor="middle"><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="337.0" transform="scale(0.1)" x="228.5" y="150">status</text><text lengthAdjust="spacing" textLength="337.0" transform="scale(0.1)" x="228.5" y="140">status</text><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="115.0" transform="scale(0.1)" x="534.5" y="150">☺</text><text lengthAdjust="spacing" textLength="115.0" transform="scale(0.1)" x="534.5" y="140">☺</text></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
1
tests/golden-images/tests.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="20" width="218.8" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect fill="#fff" height="20" rx="3" width="218.8"/></clipPath><g clip-path="url(#round)"><rect fill="#007ec6" height="20" width="36.8"/><rect fill="#fe7d37" height="20" width="182.0" x="36.8"/><rect fill="url(#smooth)" height="20" width="218.8"/></g><g fill="#fff" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110" text-anchor="middle"><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="268.0" transform="scale(0.1)" x="194.0" y="150">tests</text><text lengthAdjust="spacing" textLength="268.0" transform="scale(0.1)" x="194.0" y="140">tests</text><text fill="#010101" fill-opacity=".3" lengthAdjust="spacing" textLength="1720.0" transform="scale(0.1)" x="1268.0" y="150">231 passed, 1 failed, 1 skipped</text><text lengthAdjust="spacing" textLength="1720.0" transform="scale(0.1)" x="1268.0" y="140">231 passed, 1 failed, 1 skipped</text></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
94
tests/test-badges.json
Normal file
@@ -0,0 +1,94 @@
|
||||
[
|
||||
{
|
||||
"file_name": "accuracy.svg",
|
||||
"left_text": "accuracy",
|
||||
"right_text": "70%",
|
||||
"right_color": "yellow",
|
||||
"logo": "data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" height=\"120\" width=\"120\"><circle cx=\"60\" cy=\"60\" r=\"60\" fill=\"#c00\" /><circle cx=\"60\" cy=\"60\" r=\"50\" fill=\"#ddd\" /><circle cx=\"60\" cy=\"60\" r=\"40\" fill=\"#c00\" /><circle cx=\"60\" cy=\"60\" r=\"30\" fill=\"#ddd\" /><circle cx=\"60\" cy=\"60\" r=\"20\" fill=\"#c00\" /></svg>"
|
||||
},
|
||||
{
|
||||
"file_name": "build-passing.svg",
|
||||
"left_text": "build",
|
||||
"right_text": "passing",
|
||||
"right_color": "green"
|
||||
},
|
||||
{
|
||||
"file_name": "build-failure.svg",
|
||||
"left_text": "build",
|
||||
"right_text": "failure",
|
||||
"right_color": "#c00"
|
||||
},
|
||||
{
|
||||
"file_name": "build-running.svg",
|
||||
"left_text": "build",
|
||||
"right_text": "running",
|
||||
"right_color": "blue"
|
||||
},
|
||||
{
|
||||
"file_name": "complete.svg",
|
||||
"left_text": "complete",
|
||||
"right_text": "example",
|
||||
"left_color": "green",
|
||||
"right_color": "#fb3",
|
||||
"left_link": "http://www.complete.com/",
|
||||
"right_link": "http://www.example.com",
|
||||
"logo": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAD0lEQVQI12P4zwAD/xkYAA/+Af8iHnLUAAAAAElFTkSuQmCC"
|
||||
},
|
||||
{
|
||||
"file_name": "github.svg",
|
||||
"left_text": "github",
|
||||
"right_text": "pybadges",
|
||||
"right_color": "blue",
|
||||
"whole_link": "TODO"
|
||||
},
|
||||
{
|
||||
"file_name": "license.svg",
|
||||
"left_text": "license",
|
||||
"right_text": "APACHE 2.0",
|
||||
"right_color": "blue",
|
||||
"left_link": "https://opensource.org/licenses",
|
||||
"right_link": "https://opensource.org/licenses/Apache-2.0"
|
||||
},
|
||||
{
|
||||
"file_name": "pip.svg",
|
||||
"left_text": "pip install",
|
||||
"right_text": "pybadges",
|
||||
"right_color": "blue",
|
||||
"left_link": "https://pip.pypa.io/en/stable/installing/",
|
||||
"right_link": "https://pypi.org/project/pybadges/"
|
||||
},
|
||||
{
|
||||
"file_name": "python.svg",
|
||||
"left_text": "python",
|
||||
"right_text": "3.2, 3.3, 3.4, 3.5, 3.6",
|
||||
"logo": "https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/python.svg",
|
||||
"whole_link": "https://www.python.org/"
|
||||
},
|
||||
{
|
||||
"file_name": "saying-arabic.svg",
|
||||
"left_text": "saying",
|
||||
"right_text": " \u0623\u0628\u0627\u062F \u0627\u0644\u0644\u0647 \u062E\u0636\u0631\u0627\u0621\u0647\u0645 \u0627\u0628\u0630\u0644 \u0644\u0635\u062F\u064A\u0642\u0643 \u062F\u0645\u0643 \u0648\u0645\u0627\u0644\u0643"
|
||||
},
|
||||
{
|
||||
"file_name": "saying-chinese.svg",
|
||||
"left_text": "saying",
|
||||
"right_text": "\u4E0D\u805E\u4E0D\u82E5\u805E\u4E4B\uFF0C\u805E\u4E4B\u4E0D\u82E5\u898B\u4E4B\uFF0C\u898B\u4E4B\u4E0D\u82E5\u77E5\u4E4B\uFF0C\u77E5\u4E4B\u4E0D\u82E5\u884C\u4E4B\uFF1B\u5B78\u81F3\u65BC\u884C\u4E4B\u800C\u6B62\u77E3"
|
||||
},
|
||||
{
|
||||
"file_name": "saying-russian.svg",
|
||||
"left_text": "saying",
|
||||
"right_text": "\u0411\u0435\u0437 \u0442\u0440\u0443\u0434\u0430\u0301 \u043D\u0435 \u0432\u044B\u0301\u0442\u0430\u0449\u0438\u0448\u044C \u0438 \u0440\u044B\u0301\u0431\u043A\u0443 \u0438\u0437 \u043F\u0440\u0443\u0434\u0430\u0301."
|
||||
},
|
||||
{
|
||||
"file_name": "status.svg",
|
||||
"left_text": "status",
|
||||
"right_text": "\u263A"
|
||||
},
|
||||
{
|
||||
"file_name": "tests.svg",
|
||||
"left_text": "tests",
|
||||
"right_text": "231 passed, 1 failed, 1 skipped",
|
||||
"left_color": "blue",
|
||||
"right_color": "orange"
|
||||
}
|
||||
]
|
65
tests/test_precalculated_text_measurer.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# Copyright 2018 The pybadge Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for PrecalculatedTextMeasurer."""
|
||||
|
||||
import unittest
|
||||
|
||||
from pybadges import precalculated_text_measurer
|
||||
|
||||
|
||||
class TestPrecalculatedTextMeasurer(unittest.TestCase):
|
||||
|
||||
def test_some_known_widths(self):
|
||||
measurer = precalculated_text_measurer.PrecalculatedTextMeasurer(
|
||||
default_character_width=5.1,
|
||||
char_to_width={'H': 1.2, 'l': 1.3},
|
||||
pair_to_kern={})
|
||||
|
||||
text_width = measurer.text_width('Hello')
|
||||
self.assertAlmostEqual(text_width, 1.2 + 5.1 + 1.3 + 1.3 + 5.1)
|
||||
|
||||
def test_kern_in_middle(self):
|
||||
measurer = precalculated_text_measurer.PrecalculatedTextMeasurer(
|
||||
default_character_width=5, char_to_width={},
|
||||
pair_to_kern={'el': 3.3, 'll': 4.4, 'no': 5.5})
|
||||
|
||||
text_width = measurer.text_width('Hello')
|
||||
self.assertAlmostEqual(text_width, 5 * 5 - 3.3 - 4.4)
|
||||
|
||||
def test_kern_at_start(self):
|
||||
measurer = precalculated_text_measurer.PrecalculatedTextMeasurer(
|
||||
default_character_width=5, char_to_width={},
|
||||
pair_to_kern={'He': 3.3, 'no': 4.4})
|
||||
|
||||
text_width = measurer.text_width('Hello')
|
||||
self.assertAlmostEqual(text_width, 5 * 5 - 3.3)
|
||||
|
||||
def test_kern_at_end(self):
|
||||
measurer = precalculated_text_measurer.PrecalculatedTextMeasurer(
|
||||
default_character_width=5, char_to_width={},
|
||||
pair_to_kern={'lo': 3.3, 'no': 4.4})
|
||||
|
||||
text_width = measurer.text_width('Hello')
|
||||
self.assertAlmostEqual(text_width, 5 * 5 - 3.3)
|
||||
|
||||
def test_default_usable(self):
|
||||
measurer = (
|
||||
precalculated_text_measurer.PrecalculatedTextMeasurer
|
||||
.default())
|
||||
measurer.text_width('This is a long string of text')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
55
tests/test_pybadges.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# Copyright 2018 The pybadge Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for pybadges."""
|
||||
|
||||
import doctest
|
||||
import json
|
||||
import os.path
|
||||
import unittest
|
||||
|
||||
import pybadges
|
||||
|
||||
TEST_DIR = os.path.dirname(__file__)
|
||||
|
||||
|
||||
class TestPybadgesBadge(unittest.TestCase):
|
||||
"""Tests for pybadges.badge."""
|
||||
|
||||
def test_docs(self):
|
||||
doctest.testmod(pybadges, optionflags=doctest.ELLIPSIS)
|
||||
|
||||
def test_whole_link_and_left_link(self):
|
||||
with self.assertRaises(ValueError):
|
||||
pybadges.badge(left_text='foo', right_text='bar',
|
||||
left_link='http://example.com/',
|
||||
whole_link='http://example.com/')
|
||||
|
||||
def test_changes(self):
|
||||
with open(os.path.join(TEST_DIR, 'test-badges.json'), 'r') as f:
|
||||
examples = json.load(f)
|
||||
|
||||
for example in examples:
|
||||
file_name = example.pop('file_name')
|
||||
with self.subTest(example=file_name):
|
||||
filepath = os.path.join(TEST_DIR, 'golden-images', file_name)
|
||||
|
||||
with open(filepath, 'r') as f:
|
||||
golden_image = f.read()
|
||||
pybadge_image = pybadges.badge(**example)
|
||||
self.assertEqual(golden_image, pybadge_image)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|