Initial commit.

This commit is contained in:
Brian Quinlan
2018-06-27 13:29:57 -07:00
commit 2bbc8714ac
31 changed files with 1418 additions and 0 deletions

28
CONTRIBUTING.md Normal file
View 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
View 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
View 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:
![pip installation](tests/golden-images/pip.svg)
![pip installation](tests/golden-images/license.svg)
![pip installation](tests/golden-images/build-passing.svg)
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:
![pip installation](tests/golden-images/build-failure.svg)
## 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
```
![pip installation](tests/golden-images/complete.svg)
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'
```
![pip installation](tests/golden-images/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:
![pip installation](tests/golden-images/saying-russian.svg)
and glyphs not present in Deja Vu Sans (the default font) may
be rendered very poorly:
![pip installation](tests/golden-images/saying-chinese.svg)
- 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:
![pip installation](tests/golden-images/saying-arabic.svg)
## 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
View 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
View 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
View 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
View 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()

View 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>

Binary file not shown.

View 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

View 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()

View 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
View 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
View 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')

View 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,&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; height=&quot;120&quot; width=&quot;120&quot;&gt;&lt;circle cx=&quot;60&quot; cy=&quot;60&quot; r=&quot;60&quot; fill=&quot;#c00&quot; /&gt;&lt;circle cx=&quot;60&quot; cy=&quot;60&quot; r=&quot;50&quot; fill=&quot;#ddd&quot; /&gt;&lt;circle cx=&quot;60&quot; cy=&quot;60&quot; r=&quot;40&quot; fill=&quot;#c00&quot; /&gt;&lt;circle cx=&quot;60&quot; cy=&quot;60&quot; r=&quot;30&quot; fill=&quot;#ddd&quot; /&gt;&lt;circle cx=&quot;60&quot; cy=&quot;60&quot; r=&quot;20&quot; fill=&quot;#c00&quot; /&gt;&lt;/svg&gt;" 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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"
}
]

View 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
View 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()