]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/commitdiff
Merge pull request #1 from makermelissa/master
authorMelissa LeBlanc-Williams <melissa@adafruit.com>
Tue, 26 May 2020 15:32:21 +0000 (08:32 -0700)
committerGitHub <noreply@github.com>
Tue, 26 May 2020 15:32:21 +0000 (08:32 -0700)
Initial Commit

31 files changed:
.github/workflows/build.yml [new file with mode: 0644]
.github/workflows/release.yml [new file with mode: 0644]
.gitignore [new file with mode: 0644]
.pylintrc [new file with mode: 0644]
.readthedocs.yml [new file with mode: 0644]
CODE_OF_CONDUCT.md [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [deleted file]
README.rst [new file with mode: 0644]
displayio/__init__.py [new file with mode: 0644]
displayio/bitmap.py [new file with mode: 0644]
displayio/colorconverter.py [new file with mode: 0644]
displayio/display.py [new file with mode: 0644]
displayio/epaperdisplay.py [new file with mode: 0644]
displayio/fourwire.py [new file with mode: 0644]
displayio/group.py [new file with mode: 0644]
displayio/i2cdisplay.py [new file with mode: 0644]
displayio/ondiskbitmap.py [new file with mode: 0644]
displayio/palette.py [new file with mode: 0644]
displayio/parallelbus.py [new file with mode: 0644]
displayio/shape.py [new file with mode: 0644]
displayio/test.png [new file with mode: 0644]
displayio/tilegrid.py [new file with mode: 0644]
docs/_static/favicon.ico [new file with mode: 0644]
docs/api.rst [new file with mode: 0644]
docs/conf.py [new file with mode: 0644]
docs/index.rst [new file with mode: 0644]
fontio.py [new file with mode: 0644]
requirements.txt [new file with mode: 0644]
setup.py [new file with mode: 0644]
terminalio.py [new file with mode: 0644]

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644 (file)
index 0000000..0ba1c3f
--- /dev/null
@@ -0,0 +1,51 @@
+name: Build CI
+
+on: [pull_request, push]
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    steps:
+    - name: Dump GitHub context
+      env:
+        GITHUB_CONTEXT: ${{ toJson(github) }}
+      run: echo "$GITHUB_CONTEXT"
+    - name: Translate Repo Name For Build Tools filename_prefix
+      id: repo-name
+      run: |
+        echo ::set-output name=repo-name::Adafruit-Blinka-displayio
+    - name: Set up Python 3.6
+      uses: actions/setup-python@v1
+      with:
+        python-version: 3.6
+    - name: Versions
+      run: |
+        python3 --version
+    - name: Checkout Current Repo
+      uses: actions/checkout@v1
+      with:
+        submodules: true
+    - name: Checkout tools repo
+      uses: actions/checkout@v2
+      with:
+        repository: adafruit/actions-ci-circuitpython-libs
+        path: actions-ci
+    - name: Install dependencies
+      # (e.g. - apt-get: gettext, etc; pip: circuitpython-build-tools, requirements.txt; etc.)
+      run: |
+        source actions-ci/install.sh
+    - name: Pip install pylint, black, & Sphinx
+      run: |
+        pip install --force-reinstall pylint black==19.10b0 Sphinx sphinx-rtd-theme
+    - name: Library version
+      run: git describe --dirty --always --tags
+    - name: Check formatting
+      run: |
+        black --check --target-version=py35 .
+    - name: PyLint
+      run: |
+        pylint $( find . -path './*io/*.py' )
+        pylint $( find . -path './*io.py' )
+    - name: Build docs
+      working-directory: docs
+      run: sphinx-build -E -W -b html . _build/html
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644 (file)
index 0000000..220e398
--- /dev/null
@@ -0,0 +1,33 @@
+name: Release Actions
+
+on:
+  release:
+    types: [published]
+
+jobs:
+  upload-pypi:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v1
+    - name: Check For setup.py
+      id: need-pypi
+      run: |
+        echo ::set-output name=setup-py::$( find . -wholename './setup.py' )
+    - name: Set up Python
+      if: contains(steps.need-pypi.outputs.setup-py, 'setup.py')
+      uses: actions/setup-python@v1
+      with:
+        python-version: '3.x'
+    - name: Install dependencies
+      if: contains(steps.need-pypi.outputs.setup-py, 'setup.py')
+      run: |
+        python -m pip install --upgrade pip
+        pip install setuptools wheel twine
+    - name: Build and publish
+      if: contains(steps.need-pypi.outputs.setup-py, 'setup.py')
+      env:
+        TWINE_USERNAME: ${{ secrets.pypi_username }}
+        TWINE_PASSWORD: ${{ secrets.pypi_password }}
+      run: |
+        python setup.py sdist
+        twine upload dist/*
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..966df0f
--- /dev/null
@@ -0,0 +1,14 @@
+*.mpy
+.idea
+__pycache__
+_build
+*.pyc
+.env
+.python-version
+build*/
+bundles
+*.DS_Store
+.eggs
+dist
+**/*.egg-info
+.vscode
diff --git a/.pylintrc b/.pylintrc
new file mode 100644 (file)
index 0000000..d8f0ee8
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,433 @@
+[MASTER]
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code
+extension-pkg-whitelist=
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Add files or directories matching the regex patterns to the blacklist. The
+# regex matches against base names, not paths.
+ignore-patterns=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Use multiple processes to speed up Pylint.
+# jobs=1
+jobs=2
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Specify a configuration file.
+#rcfile=
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
+confidence=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+# disable=import-error,print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call
+disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,import-error,bad-continuation
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=
+
+
+[REPORTS]
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details
+#msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio).You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages
+reports=no
+
+# Activate the evaluation score.
+score=yes
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging
+
+
+[SPELLING]
+
+# Spelling dictionary name. Available dictionaries: none. To make it working
+# install python-enchant package.
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to indicated private dictionary in
+# --spelling-private-dict-file option instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+# notes=FIXME,XXX,TODO
+notes=FIXME,XXX
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis. It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=board
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,_cb
+
+# A regular expression matching the name of dummy variables (i.e. expectedly
+# not used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,future.builtins
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+# expected-line-ending-format=
+expected-line-ending-format=LF
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )?<?https?://\S+>?$
+
+# Number of spaces of indent required inside a hanging  or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually "    " (4 spaces) or "\t" (1
+# tab).
+indent-string='    '
+
+# Maximum number of characters on a single line.
+max-line-length=100
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# List of optional constructs for which whitespace checking is disabled. `dict-
+# separator` is used to allow tabulation in dicts, etc.: {1  : 1,\n222: 2}.
+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
+# `empty-line` allows space-only lines.
+no-space-check=trailing-comma,dict-separator
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+
+[BASIC]
+
+# Naming hint for argument names
+argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct argument names
+argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Naming hint for attribute names
+attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct attribute names
+attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Naming hint for class attribute names
+class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+
+# Regular expression matching correct class attribute names
+class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+
+# Naming hint for class names
+# class-name-hint=[A-Z_][a-zA-Z0-9]+$
+class-name-hint=[A-Z_][a-zA-Z0-9_]+$
+
+# Regular expression matching correct class names
+# class-rgx=[A-Z_][a-zA-Z0-9]+$
+class-rgx=[A-Z_][a-zA-Z0-9_]+$
+
+# Naming hint for constant names
+const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Regular expression matching correct constant names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming hint for function names
+function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct function names
+function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Good variable names which should always be accepted, separated by a comma
+# good-names=i,j,k,ex,Run,_
+good-names=r,g,b,w,i,j,k,n,x,y,z,ex,ok,Run,_
+
+# Include a hint for the correct naming format with invalid-name
+include-naming-hint=no
+
+# Naming hint for inline iteration names
+inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
+
+# Regular expression matching correct inline iteration names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Naming hint for method names
+method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct method names
+method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Naming hint for module names
+module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression matching correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+property-classes=abc.abstractproperty
+
+# Naming hint for variable names
+variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct variable names
+variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+
+[IMPORTS]
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=optparse,tkinter.tix
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,_fields,_replace,_source,_make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Maximum number of attributes for a class (see R0902).
+# max-attributes=7
+max-attributes=11
+
+# Maximum number of boolean expressions in a if statement
+max-bool-expr=5
+
+# Maximum number of branch for function / method body
+max-branches=12
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=1
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644 (file)
index 0000000..f4243ad
--- /dev/null
@@ -0,0 +1,3 @@
+python:
+    version: 3
+requirements_file: requirements.txt
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644 (file)
index 0000000..a62e132
--- /dev/null
@@ -0,0 +1,129 @@
+# Adafruit Community Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and leaders pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level or type of
+experience, education, socio-economic status, nationality, personal appearance,
+race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+We are committed to providing a friendly, safe and welcoming environment for
+all.
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Be kind and courteous to others
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Collaborating with other community members
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and sexual attention or advances
+* The use of inappropriate images, including in a community member's avatar
+* The use of inappropriate language, including in a community member's nickname
+* Any spamming, flaming, baiting or other attention-stealing behavior
+* Excessive or unwelcome helping; answering outside the scope of the question
+  asked
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Promoting or spreading disinformation, lies, or conspiracy theories against
+  a person, group, organisation, project, or community
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+  address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate
+
+The goal of the standards and moderation guidelines outlined here is to build
+and maintain a respectful community. We ask that you don’t just aim to be
+"technically unimpeachable", but rather try to be your best self.
+
+We value many things beyond technical expertise, including collaboration and
+supporting others within our community. Providing a positive experience for
+other community members can have a much more significant impact than simply
+providing the correct answer.
+
+## Our Responsibilities
+
+Project leaders are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project leaders have the right and responsibility to remove, edit, or
+reject messages, comments, commits, code, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any community member for other behaviors that they deem
+inappropriate, threatening, offensive, or harmful.
+
+## Moderation
+
+Instances of behaviors that violate the Adafruit Community Code of Conduct
+may be reported by any member of the community. Community members are
+encouraged to report these situations, including situations they witness
+involving other community members.
+
+You may report in the following ways:
+
+In any situation, you may send an email to <support@adafruit.com>.
+
+On the Adafruit Discord, you may send an open message from any channel
+to all Community Moderators by tagging @community moderators. You may
+also send an open message from any channel, or a direct message to
+@kattni#1507, @tannewt#4653, @Dan Halbert#1614, @cater#2442,
+@sommersoft#0222, @Mr. Certainly#0472 or @Andon#8175.
+
+Email and direct message reports will be kept confidential.
+
+In situations on Discord where the issue is particularly egregious, possibly
+illegal, requires immediate action, or violates the Discord terms of service,
+you should also report the message directly to Discord.
+
+These are the steps for upholding our community’s standards of conduct.
+
+1. Any member of the community may report any situation that violates the
+Adafruit Community Code of Conduct. All reports will be reviewed and
+investigated.
+2. If the behavior is an egregious violation, the community member who
+committed the violation may be banned immediately, without warning.
+3. Otherwise, moderators will first respond to such behavior with a warning.
+4. Moderators follow a soft "three strikes" policy - the community member may
+be given another chance, if they are receptive to the warning and change their
+behavior.
+5. If the community member is unreceptive or unreasonable when warned by a
+moderator, or the warning goes unheeded, they may be banned for a first or
+second offense. Repeated offenses will result in the community member being
+banned.
+
+## Scope
+
+This Code of Conduct and the enforcement policies listed above apply to all
+Adafruit Community venues. This includes but is not limited to any community
+spaces (both public and private), the entire Adafruit Discord server, and
+Adafruit GitHub repositories. Examples of Adafruit Community spaces include
+but are not limited to meet-ups, audio chats on the Adafruit Discord, or
+interaction at a conference.
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. As a community
+member, you are representing our community, and are expected to behave
+accordingly.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 1.4, available at
+<https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>,
+and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html).
+
+For other projects adopting the Adafruit Community Code of
+Conduct, please contact the maintainers of those projects for enforcement.
+If you wish to use this code of conduct for your own project, consider
+explicitly mentioning your moderation policy or making a copy with your
+own moderation policy so as to avoid confusion.
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..aa6d192
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
deleted file mode 100644 (file)
index c154ebe..0000000
--- a/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-# Adafruit_Blinka_displayio
-Displayio for Blinka
diff --git a/README.rst b/README.rst
new file mode 100644 (file)
index 0000000..dbaa736
--- /dev/null
@@ -0,0 +1,64 @@
+Introduction
+============
+
+.. image:: https://readthedocs.org/projects/adafruit-blinka-blinka-displayio/badge/?version=latest
+    :target: https://circuitpython.readthedocs.io/projects/displayio/en/latest/
+    :alt: Documentation Status
+
+.. image:: https://img.shields.io/discord/327254708534116352.svg
+    :target: https://discord.gg/nBQh6qu
+    :alt: Discord
+
+.. image:: https://github.com/adafruit/Adafruit_blinka_CircuitPython_displayio/workflows/Build%20CI/badge.svg
+    :target: https://github.com/adafruit/Adafruit_blinka_CircuitPython_displayio/actions
+    :alt: Build Status
+
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+    :target: https://github.com/psf/black
+    :alt: Code Style: Black
+
+displayio for Blinka
+
+
+Dependencies
+=============
+This driver depends on:
+
+* `Adafruit Blinka <https://github.com/adafruit/Adafruit_Blinka>`_
+
+Installing from PyPI
+=====================
+
+On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from
+PyPI <https://pypi.org/project/adafruit-blinka-displayio/>`_. To install for current user:
+
+.. code-block:: shell
+
+    pip3 install adafruit-blinka-displayio
+
+To install system-wide (this may be required in some cases):
+
+.. code-block:: shell
+
+    sudo pip3 install adafruit-blinka-displayio
+
+To install in a virtual environment in your current project:
+
+.. code-block:: shell
+
+    mkdir project-name && cd project-name
+    python3 -m venv .env
+    source .env/bin/activate
+    pip3 install adafruit-blinka-displayio
+
+Contributing
+============
+
+Contributions are welcome! Please read our `Code of Conduct
+<https://github.com/adafruit/Adafruit_blinka_CircuitPython_displayio/blob/master/CODE_OF_CONDUCT.md>`_
+before contributing to help this project stay welcoming.
+
+Documentation
+=============
+
+For information on building library documentation, please check out `this guide <https://learn.adafruit.com/creating-and-sharing-a-blinka-library/sharing-our-docs-on-readthedocs#sphinx-5-1>`_.
diff --git a/displayio/__init__.py b/displayio/__init__.py
new file mode 100644 (file)
index 0000000..c3b04ac
--- /dev/null
@@ -0,0 +1,64 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+`displayio`
+================================================================================
+
+displayio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+from displayio.bitmap import Bitmap
+from displayio.colorconverter import ColorConverter
+from displayio.display import Display
+from displayio.epaperdisplay import EPaperDisplay
+from displayio.fourwire import FourWire
+from displayio.group import Group
+from displayio.i2cdisplay import I2CDisplay
+from displayio.ondiskbitmap import OnDiskBitmap
+from displayio.palette import Palette
+from displayio.parallelbus import ParallelBus
+from displayio.shape import Shape
+from displayio.tilegrid import TileGrid
+from displayio.display import displays
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
+
+def release_displays():
+    """Releases any actively used displays so their busses and pins can be used again.
+
+    Use this once in your code.py if you initialize a display. Place it right before the
+    initialization so the display is active as long as possible.
+    """
+    for _disp in displays:
+        _disp._release()  # pylint: disable=protected-access
+    displays.clear()
diff --git a/displayio/bitmap.py b/displayio/bitmap.py
new file mode 100644 (file)
index 0000000..1bc90aa
--- /dev/null
@@ -0,0 +1,139 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+`displayio.bitmap`
+================================================================================
+
+displayio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+from recordclass import recordclass
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
+Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
+
+
+class Bitmap:
+    """Stores values of a certain size in a 2D array"""
+
+    def __init__(self, width, height, value_count):
+        """Create a Bitmap object with the given fixed size. Each pixel stores a value that is
+        used to index into a corresponding palette. This enables differently colored sprites to
+        share the underlying Bitmap. value_count is used to minimize the memory used to store
+        the Bitmap.
+        """
+        self._width = width
+        self._height = height
+        self._read_only = False
+
+        if value_count < 0:
+            raise ValueError("value_count must be > 0")
+
+        bits = 1
+        while (value_count - 1) >> bits:
+            if bits < 8:
+                bits = bits << 1
+            else:
+                bits += 8
+
+        self._bits_per_value = bits
+
+        if (
+            self._bits_per_value > 8
+            and self._bits_per_value != 16
+            and self._bits_per_value != 32
+        ):
+            raise NotImplementedError("Invalid bits per value")
+
+        self._data = (width * height) * [0]
+        self._dirty_area = Rectangle(0, 0, width, height)
+
+    def __getitem__(self, index):
+        """
+        Returns the value at the given index. The index can either be
+        an x,y tuple or an int equal to `y * width + x`.
+        """
+        if isinstance(index, (tuple, list)):
+            index = (index[1] * self._width) + index[0]
+        if index >= len(self._data):
+            raise ValueError("Index {} is out of range".format(index))
+        return self._data[index]
+
+    def __setitem__(self, index, value):
+        """
+        Sets the value at the given index. The index can either be
+        an x,y tuple or an int equal to `y * width + x`.
+        """
+        if self._read_only:
+            raise RuntimeError("Read-only object")
+        if isinstance(index, (tuple, list)):
+            x = index[0]
+            y = index[1]
+            index = y * self._width + x
+        elif isinstance(index, int):
+            x = index % self._width
+            y = index // self._width
+        self._data[index] = value
+        if self._dirty_area.x1 == self._dirty_area.x2:
+            self._dirty_area.x1 = x
+            self._dirty_area.x2 = x + 1
+            self._dirty_area.y1 = y
+            self._dirty_area.y2 = y + 1
+        else:
+            if x < self._dirty_area.x1:
+                self._dirty_area.x1 = x
+            elif x >= self._dirty_area.x2:
+                self._dirty_area.x2 = x + 1
+            if y < self._dirty_area.y1:
+                self._dirty_area.y1 = y
+            elif y >= self._dirty_area.y2:
+                self._dirty_area.y2 = y + 1
+
+    def _finish_refresh(self):
+        self._dirty_area.x1 = 0
+        self._dirty_area.x2 = 0
+
+    def fill(self, value):
+        """Fills the bitmap with the supplied palette index value."""
+        self._data = (self._width * self._height) * [value]
+        self._dirty_area = Rectangle(0, 0, self._width, self._height)
+
+    @property
+    def width(self):
+        """Width of the bitmap. (read only)"""
+        return self._width
+
+    @property
+    def height(self):
+        """Height of the bitmap. (read only)"""
+        return self._height
diff --git a/displayio/colorconverter.py b/displayio/colorconverter.py
new file mode 100644 (file)
index 0000000..ead3f34
--- /dev/null
@@ -0,0 +1,124 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+`displayio.colorconverter`
+================================================================================
+
+displayio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
+
+class ColorConverter:
+    """Converts one color format to another. Color converter based on original displayio
+    code for consistency.
+    """
+
+    def __init__(self, *, dither=False):
+        """Create a ColorConverter object to convert color formats.
+        Only supports rgb888 to RGB565 currently.
+        :param bool dither: Adds random noise to dither the output image
+        """
+        self._dither = dither
+        self._depth = 16
+
+    # pylint: disable=no-self-use
+    def _compute_rgb565(self, color):
+        self._depth = 16
+        return (color >> 19) << 11 | ((color >> 10) & 0x3F) << 5 | (color >> 3) & 0x1F
+
+    def _compute_luma(self, color):
+        red = color >> 16
+        green = (color >> 8) & 0xFF
+        blue = color & 0xFF
+        return (red * 19) / 255 + (green * 182) / 255 + (blue + 54) / 255
+
+    def _compute_chroma(self, color):
+        red = color >> 16
+        green = (color >> 8) & 0xFF
+        blue = color & 0xFF
+        return max(red, green, blue) - min(red, green, blue)
+
+    def _compute_hue(self, color):
+        red = color >> 16
+        green = (color >> 8) & 0xFF
+        blue = color & 0xFF
+        max_color = max(red, green, blue)
+        chroma = self._compute_chroma(color)
+        if chroma == 0:
+            return 0
+        hue = 0
+        if max_color == red:
+            hue = (((green - blue) * 40) / chroma) % 240
+        elif max_color == green:
+            hue = (((blue - red) + (2 * chroma)) * 40) / chroma
+        elif max_color == blue:
+            hue = (((red - green) + (4 * chroma)) * 40) / chroma
+        if hue < 0:
+            hue += 240
+
+        return hue
+
+    def _dither_noise_1(self, noise):
+        noise = (noise >> 13) ^ noise
+        more_noise = (
+            noise * (noise * noise * 60493 + 19990303) + 1376312589
+        ) & 0x7FFFFFFF
+        return (more_noise / (1073741824.0 * 2)) * 255
+
+    def _dither_noise_2(self, x, y):
+        return self._dither_noise_1(x + y * 0xFFFF)
+
+    def _compute_tricolor(self):
+        pass
+
+    def convert(self, color):
+        "Converts the given rgb888 color to RGB565"
+        if self._dither:
+            return color  # To Do: return a dithered color
+        return self._compute_rgb565(color)
+
+    # pylint: enable=no-self-use
+
+    @property
+    def dither(self):
+        """When true the color converter dithers the output by adding
+        random noise when truncating to display bitdepth
+        """
+        return self._dither
+
+    @dither.setter
+    def dither(self, value):
+        if not isinstance(value, bool):
+            raise ValueError("Value should be boolean")
+        self._dither = value
diff --git a/displayio/display.py b/displayio/display.py
new file mode 100644 (file)
index 0000000..4325671
--- /dev/null
@@ -0,0 +1,324 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+`displayio.display`
+================================================================================
+
+displayio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+import time
+import struct
+import threading
+from PIL import Image
+import numpy
+from recordclass import recordclass
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
+Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
+displays = []
+
+# pylint: disable=unnecessary-pass, unused-argument
+
+# pylint: disable=too-many-instance-attributes
+class Display:
+    """This initializes a display and connects it into CircuitPython. Unlike other objects
+    in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
+    This is done so that CircuitPython can use the display itself.
+
+    Most people should not use this class directly. Use a specific display driver instead
+    that will contain the initialization sequence at minimum.
+
+    .. class::
+        Display(display_bus, init_sequence, *, width, height, colstart=0, rowstart=0, rotation=0,
+        color_depth=16, grayscale=False, pixels_in_byte_share_row=True, bytes_per_cell=1,
+        reverse_pixels_in_byte=False, set_column_command=0x2a, set_row_command=0x2b,
+        write_ram_command=0x2c, set_vertical_scroll=0, backlight_pin=None, brightness_command=None,
+        brightness=1.0, auto_brightness=False, single_byte_bounds=False, data_as_commands=False,
+        auto_refresh=True, native_frames_per_second=60)
+    """
+
+    # pylint: disable=too-many-locals
+    def __init__(
+        self,
+        display_bus,
+        init_sequence,
+        *,
+        width,
+        height,
+        colstart=0,
+        rowstart=0,
+        rotation=0,
+        color_depth=16,
+        grayscale=False,
+        pixels_in_byte_share_row=True,
+        bytes_per_cell=1,
+        reverse_pixels_in_byte=False,
+        set_column_command=0x2A,
+        set_row_command=0x2B,
+        write_ram_command=0x2C,
+        set_vertical_scroll=0,
+        backlight_pin=None,
+        brightness_command=None,
+        brightness=1.0,
+        auto_brightness=False,
+        single_byte_bounds=False,
+        data_as_commands=False,
+        auto_refresh=True,
+        native_frames_per_second=60
+    ):
+        """Create a Display object on the given display bus (`displayio.FourWire` or
+        `displayio.ParallelBus`).
+
+        The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
+        with a command byte followed by a byte to determine the parameter count and if a
+        delay is need after. When the top bit of the second byte is 1, the next byte will be
+        the delay time in milliseconds. The remaining 7 bits are the parameter count
+        excluding any delay byte. The third through final bytes are the remaining command
+        parameters. The next byte will begin a new command definition. Here is a portion of
+        ILI9341 init code:
+        .. code-block:: python
+
+            init_sequence = (
+                b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F"
+                b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms)
+                b"\x29\x80\x78"# Display on then delay 0x78 (120ms)
+            )
+            display = displayio.Display(display_bus, init_sequence, width=320, height=240)
+
+        The first command is 0xe1 with 15 (0xf) parameters following. The second and third
+        are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
+        Multiple byte literals (b”“) are merged together on load. The parens are needed to
+        allow byte literals on subsequent lines.
+
+        The initialization sequence should always leave the display memory access inline with
+        the scan of the display to minimize tearing artifacts.
+        """
+        self._bus = display_bus
+        self._set_column_command = set_column_command
+        self._set_row_command = set_row_command
+        self._write_ram_command = write_ram_command
+        self._brightness_command = brightness_command
+        self._data_as_commands = data_as_commands
+        self._single_byte_bounds = single_byte_bounds
+        self._width = width
+        self._height = height
+        self._colstart = colstart
+        self._rowstart = rowstart
+        self._rotation = rotation
+        self._auto_brightness = auto_brightness
+        self._brightness = brightness
+        self._auto_refresh = auto_refresh
+        self._initialize(init_sequence)
+        self._buffer = Image.new("RGB", (width, height))
+        self._subrectangles = []
+        self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
+        self._current_group = None
+        displays.append(self)
+        self._refresh_thread = None
+        if self._auto_refresh:
+            self.auto_refresh = True
+
+    # pylint: enable=too-many-locals
+
+    def _initialize(self, init_sequence):
+        i = 0
+        while i < len(init_sequence):
+            command = init_sequence[i]
+            data_size = init_sequence[i + 1]
+            delay = (data_size & 0x80) > 0
+            data_size &= ~0x80
+            self._write(command, init_sequence[i + 2 : i + 2 + data_size])
+            delay_time_ms = 10
+            if delay:
+                data_size += 1
+                delay_time_ms = init_sequence[i + 1 + data_size]
+                if delay_time_ms == 255:
+                    delay_time_ms = 500
+            time.sleep(delay_time_ms / 1000)
+            i += 2 + data_size
+
+    def _write(self, command, data):
+        if self._single_byte_bounds:
+            self._bus.send(True, bytes([command]) + data, toggle_every_byte=True)
+        else:
+            self._bus.send(True, bytes([command]), toggle_every_byte=True)
+            self._bus.send(False, data)
+
+    def _release(self):
+        self._bus.release()
+        self._bus = None
+
+    def show(self, group):
+        """Switches to displaying the given group of layers. When group is None, the
+        default CircuitPython terminal will be shown.
+        """
+        self._current_group = group
+
+    def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
+        """When auto refresh is off, waits for the target frame rate and then refreshes the
+        display, returning True. If the call has taken too long since the last refresh call
+        for the given target frame rate, then the refresh returns False immediately without
+        updating the screen to hopefully help getting caught up.
+
+        If the time since the last successful refresh is below the minimum frame rate, then
+        an exception will be raised. Set minimum_frames_per_second to 0 to disable.
+
+        When auto refresh is on, updates the display immediately. (The display will also
+        update without calls to this.)
+        """
+        # Go through groups and and add each to buffer
+        if self._current_group is not None:
+            buffer = Image.new("RGBA", (self._width, self._height))
+            # Recursively have everything draw to the image
+            self._current_group._fill_area(buffer)  # pylint: disable=protected-access
+            # save image to buffer (or probably refresh buffer so we can compare)
+            self._buffer.paste(buffer)
+        time.sleep(1)
+        # Eventually calculate dirty rectangles here
+        self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
+
+        for area in self._subrectangles:
+            self._refresh_display_area(area)
+
+    def _refresh_loop(self):
+        while self._auto_refresh:
+            self.refresh()
+
+    def _refresh_display_area(self, rectangle):
+        """Loop through dirty rectangles and redraw that area."""
+        data = numpy.array(self._buffer.crop(rectangle).convert("RGB")).astype("uint16")
+        color = (
+            ((data[:, :, 0] & 0xF8) << 8)
+            | ((data[:, :, 1] & 0xFC) << 3)
+            | (data[:, :, 2] >> 3)
+        )
+
+        pixels = list(
+            numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
+        )
+
+        self._write(
+            self._set_column_command,
+            self._encode_pos(
+                rectangle.x1 + self._colstart, rectangle.x2 + self._colstart
+            ),
+        )
+        self._write(
+            self._set_row_command,
+            self._encode_pos(
+                rectangle.y1 + self._rowstart, rectangle.y2 + self._rowstart
+            ),
+        )
+        self._write(self._write_ram_command, pixels)
+
+    def _encode_pos(self, x, y):
+        """Encode a postion into bytes."""
+        return struct.pack(self._bounds_encoding, x, y)
+
+    def fill_row(self, y, buffer):
+        """Extract the pixels from a single row"""
+        pass
+
+    @property
+    def auto_refresh(self):
+        """True when the display is refreshed automatically."""
+        return self._auto_refresh
+
+    @auto_refresh.setter
+    def auto_refresh(self, value):
+        self._auto_refresh = value
+        if self._refresh_thread is None:
+            self._refresh_thread = threading.Thread(
+                target=self._refresh_loop, daemon=True
+            )
+        if value and not self._refresh_thread.is_alive():
+            # Start the thread
+            self._refresh_thread.start()
+        elif not value and self._refresh_thread.is_alive():
+            # Stop the thread
+            self._refresh_thread.join()
+
+    @property
+    def brightness(self):
+        """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
+        When `auto_brightness` is True, the value of `brightness` will change automatically.
+        If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
+        """
+        return self._brightness
+
+    @brightness.setter
+    def brightness(self, value):
+        self._brightness = value
+
+    @property
+    def auto_brightness(self):
+        """True when the display brightness is adjusted automatically, based on an ambient
+        light sensor or other method. Note that some displays may have this set to True by
+        default, but not actually implement automatic brightness adjustment.
+        `auto_brightness` is set to False if `brightness` is set manually.
+        """
+        return self._auto_brightness
+
+    @auto_brightness.setter
+    def auto_brightness(self, value):
+        self._auto_brightness = value
+
+    @property
+    def width(self):
+        """Display Width"""
+        return self._width
+
+    @property
+    def height(self):
+        """Display Height"""
+        return self._height
+
+    @property
+    def rotation(self):
+        """The rotation of the display as an int in degrees."""
+        return self._rotation
+
+    @rotation.setter
+    def rotation(self, value):
+        if value not in (0, 90, 180, 270):
+            raise ValueError("Rotation must be 0/90/180/270")
+        self._rotation = value
+
+    @property
+    def bus(self):
+        """Current Display Bus"""
+        return self._bus
+
+
+# pylint: enable=too-many-instance-attributes
diff --git a/displayio/epaperdisplay.py b/displayio/epaperdisplay.py
new file mode 100644 (file)
index 0000000..a0946c2
--- /dev/null
@@ -0,0 +1,880 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+`displayio.epaperdisplay`
+================================================================================
+
+displayio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+import time
+import digitalio
+from recordclass import recordclass
+from PIL import Image
+from displayio.bitmap import Bitmap
+from displayio.colorconverter import ColorConverter
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
+# pylint: disable=unnecessary-pass, unused-argument
+
+Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
+Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
+
+
+class EPaperDisplay:
+    """Manage updating an epaper display over a display bus
+
+    This initializes an epaper display and connects it into CircuitPython. Unlike other
+    objects in CircuitPython, EPaperDisplay objects live until
+    displayio.release_displays() is called. This is done so that CircuitPython can use
+    the display itself.
+
+    Most people should not use this class directly. Use a specific display driver instead
+    that will contain the startup and shutdown sequences at minimum.
+    """
+
+    # pylint: disable=too-many-locals
+    def __init__(
+        self,
+        display_bus,
+        start_sequence,
+        stop_sequence,
+        *,
+        width,
+        height,
+        ram_width,
+        ram_height,
+        colstart=0,
+        rowstart=0,
+        rotation=0,
+        set_column_window_command=None,
+        set_row_window_command=None,
+        single_byte_bounds=False,
+        write_black_ram_command,
+        black_bits_inverted=False,
+        write_color_ram_command=None,
+        color_bits_inverted=False,
+        highlight_color=0x000000,
+        refresh_display_command,
+        refresh_time=40,
+        busy_pin=None,
+        busy_state=True,
+        seconds_per_frame=180,
+        always_toggle_chip_select=False
+    ):
+        """
+        Create a EPaperDisplay object on the given display bus (displayio.FourWire or
+        displayio.ParallelBus).
+
+        The start_sequence and stop_sequence are bitpacked to minimize the ram impact. Every
+        command begins with a command byte followed by a byte to determine the parameter
+        count and if a delay is need after. When the top bit of the second byte is 1, the
+        next byte will be the delay time in milliseconds. The remaining 7 bits are the
+        parameter count excluding any delay byte. The third through final bytes are the
+        remaining command parameters. The next byte will begin a new command definition.
+        """
+        pass
+
+    # pylint: enable=too-many-locals
+
+    def show(self, group):
+        """Switches to displaying the given group of layers. When group is None, the default
+        CircuitPython terminal will be shown (eventually).
+        """
+        pass
+
+    def refresh(self):
+        """Refreshes the display immediately or raises an exception if too soon. Use
+        ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur.
+        """
+        pass
+
+    @property
+    def time_to_refresh(self):
+        """Time, in fractional seconds, until the ePaper display can be refreshed."""
+        return 0
+
+    @property
+    def width(self):
+        """Display Width"""
+        pass
+
+    @property
+    def height(self):
+        """Display Height"""
+        pass
+
+    @property
+    def bus(self):
+        """Current Display Bus"""
+        pass
+
+
+class FourWire:
+    """Manage updating a display over SPI four wire protocol in the background while
+    Python code runs. It doesn’t handle display initialization.
+    """
+
+    def __init__(
+        self,
+        spi_bus,
+        *,
+        command,
+        chip_select,
+        reset=None,
+        baudrate=24000000,
+        polarity=0,
+        phase=0
+    ):
+        """Create a FourWire object associated with the given pins.
+
+        The SPI bus and pins are then in use by the display until
+        displayio.release_displays() is called even after a reload. (It does this so
+        CircuitPython can use the display after your code is done.)
+        So, the first time you initialize a display bus in code.py you should call
+        :py:func`displayio.release_displays` first, otherwise it will error after the
+        first code.py run.
+        """
+        self._dc = digitalio.DigitalInOut(command)
+        self._dc.switch_to_output()
+        self._chip_select = digitalio.DigitalInOut(chip_select)
+        self._chip_select.switch_to_output(value=True)
+
+        if reset is not None:
+            self._reset = digitalio.DigitalInOut(reset)
+            self._reset.switch_to_output(value=True)
+        else:
+            self._reset = None
+        self._spi = spi_bus
+        while self._spi.try_lock():
+            pass
+        self._spi.configure(baudrate=baudrate, polarity=polarity, phase=phase)
+        self._spi.unlock()
+
+    def _release(self):
+        self.reset()
+        self._spi.deinit()
+        self._dc.deinit()
+        self._chip_select.deinit()
+        if self._reset is not None:
+            self._reset.deinit()
+
+    def reset(self):
+        """Performs a hardware reset via the reset pin.
+        Raises an exception if called when no reset pin is available.
+        """
+        if self._reset is not None:
+            self._reset.value = False
+            time.sleep(0.001)
+            self._reset.value = True
+            time.sleep(0.001)
+        else:
+            raise RuntimeError("No reset pin defined")
+
+    def send(self, is_command, data, *, toggle_every_byte=False):
+        """Sends the given command value followed by the full set of data. Display state,
+        such as vertical scroll, set via ``send`` may or may not be reset once the code is
+        done.
+        """
+        while self._spi.try_lock():
+            pass
+        self._dc.value = not is_command
+        if toggle_every_byte:
+            for byte in data:
+                self._spi.write(bytes([byte]))
+                self._chip_select.value = True
+                time.sleep(0.000001)
+                self._chip_select.value = False
+        else:
+            self._spi.write(data)
+        self._spi.unlock()
+
+
+class Group:
+    """Manage a group of sprites and groups and how they are inter-related."""
+
+    def __init__(self, *, max_size=4, scale=1, x=0, y=0):
+        """Create a Group of a given size and scale. Scale is in
+        one dimension. For example, scale=2 leads to a layer’s
+        pixel being 2x2 pixels when in the group.
+        """
+        if not isinstance(max_size, int) or max_size < 1:
+            raise ValueError("Max Size must be >= 1")
+        self._max_size = max_size
+        if not isinstance(scale, int) or scale < 1:
+            raise ValueError("Scale must be >= 1")
+        self._scale = scale
+        self._x = x
+        self._y = y
+        self._hidden = False
+        self._layers = []
+        self._supported_types = (TileGrid, Group)
+        self._absolute_transform = None
+        self.in_group = False
+        self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
+
+    def update_transform(self, parent_transform):
+        """Update the parent transform and child transforms"""
+        self.in_group = parent_transform is not None
+        if self.in_group:
+            x = self._x
+            y = self._y
+            if parent_transform.transpose_xy:
+                x, y = y, x
+            self._absolute_transform.x = parent_transform.x + parent_transform.dx * x
+            self._absolute_transform.y = parent_transform.y + parent_transform.dy * y
+            self._absolute_transform.dx = parent_transform.dx * self._scale
+            self._absolute_transform.dy = parent_transform.dy * self._scale
+            self._absolute_transform.transpose_xy = parent_transform.transpose_xy
+            self._absolute_transform.mirror_x = parent_transform.mirror_x
+            self._absolute_transform.mirror_y = parent_transform.mirror_y
+            self._absolute_transform.scale = parent_transform.scale * self._scale
+        self._update_child_transforms()
+
+    def _update_child_transforms(self):
+        if self.in_group:
+            for layer in self._layers:
+                layer.update_transform(self._absolute_transform)
+
+    def _removal_cleanup(self, index):
+        layer = self._layers[index]
+        layer.update_transform(None)
+
+    def _layer_update(self, index):
+        layer = self._layers[index]
+        layer.update_transform(self._absolute_transform)
+
+    def append(self, layer):
+        """Append a layer to the group. It will be drawn
+        above other layers.
+        """
+        self.insert(len(self._layers), layer)
+
+    def insert(self, index, layer):
+        """Insert a layer into the group."""
+        if not isinstance(layer, self._supported_types):
+            raise ValueError("Invalid Group Member")
+        if layer.in_group:
+            raise ValueError("Layer already in a group.")
+        if len(self._layers) == self._max_size:
+            raise RuntimeError("Group full")
+        self._layers.insert(index, layer)
+        self._layer_update(index)
+
+    def index(self, layer):
+        """Returns the index of the first copy of layer.
+        Raises ValueError if not found.
+        """
+        return self._layers.index(layer)
+
+    def pop(self, index=-1):
+        """Remove the ith item and return it."""
+        self._removal_cleanup(index)
+        return self._layers.pop(index)
+
+    def remove(self, layer):
+        """Remove the first copy of layer. Raises ValueError
+        if it is not present."""
+        index = self.index(layer)
+        self._layers.pop(index)
+
+    def __len__(self):
+        """Returns the number of layers in a Group"""
+        return len(self._layers)
+
+    def __getitem__(self, index):
+        """Returns the value at the given index."""
+        return self._layers[index]
+
+    def __setitem__(self, index, value):
+        """Sets the value at the given index."""
+        self._removal_cleanup(index)
+        self._layers[index] = value
+        self._layer_update(index)
+
+    def __delitem__(self, index):
+        """Deletes the value at the given index."""
+        del self._layers[index]
+
+    def _fill_area(self, buffer):
+        if self._hidden:
+            return
+
+        for layer in self._layers:
+            if isinstance(layer, (Group, TileGrid)):
+                layer._fill_area(buffer)  # pylint: disable=protected-access
+
+    @property
+    def hidden(self):
+        """True when the Group and all of it’s layers are not visible. When False, the
+        Group’s layers are visible if they haven’t been hidden.
+        """
+        return self._hidden
+
+    @hidden.setter
+    def hidden(self, value):
+        if not isinstance(value, (bool, int)):
+            raise ValueError("Expecting a boolean or integer value")
+        self._hidden = bool(value)
+
+    @property
+    def scale(self):
+        """Scales each pixel within the Group in both directions. For example, when
+        scale=2 each pixel will be represented by 2x2 pixels.
+        """
+        return self._scale
+
+    @scale.setter
+    def scale(self, value):
+        if not isinstance(value, int) or value < 1:
+            raise ValueError("Scale must be >= 1")
+        if self._scale != value:
+            parent_scale = self._absolute_transform.scale / self._scale
+            self._absolute_transform.dx = (
+                self._absolute_transform.dx / self._scale * value
+            )
+            self._absolute_transform.dy = (
+                self._absolute_transform.dy / self._scale * value
+            )
+            self._absolute_transform.scale = parent_scale * value
+
+            self._scale = value
+            self._update_child_transforms()
+
+    @property
+    def x(self):
+        """X position of the Group in the parent."""
+        return self._x
+
+    @x.setter
+    def x(self, value):
+        if not isinstance(value, int):
+            raise ValueError("x must be an integer")
+        if self._x != value:
+            if self._absolute_transform.transpose_xy:
+                dy_value = self._absolute_transform.dy / self._scale
+                self._absolute_transform.y += dy_value * (value - self._x)
+            else:
+                dx_value = self._absolute_transform.dx / self._scale
+                self._absolute_transform.x += dx_value * (value - self._x)
+            self._x = value
+            self._update_child_transforms()
+
+    @property
+    def y(self):
+        """Y position of the Group in the parent."""
+        return self._y
+
+    @y.setter
+    def y(self, value):
+        if not isinstance(value, int):
+            raise ValueError("y must be an integer")
+        if self._y != value:
+            if self._absolute_transform.transpose_xy:
+                dx_value = self._absolute_transform.dx / self._scale
+                self._absolute_transform.x += dx_value * (value - self._y)
+            else:
+                dy_value = self._absolute_transform.dy / self._scale
+                self._absolute_transform.y += dy_value * (value - self._y)
+            self._y = value
+            self._update_child_transforms()
+
+
+class I2CDisplay:
+    """Manage updating a display over I2C in the background while Python code runs.
+    It doesn’t handle display initialization.
+    """
+
+    def __init__(self, i2c_bus, *, device_address, reset=None):
+        """Create a I2CDisplay object associated with the given I2C bus and reset pin.
+
+        The I2C bus and pins are then in use by the display until displayio.release_displays() is
+        called even after a reload. (It does this so CircuitPython can use the display after your
+        code is done.) So, the first time you initialize a display bus in code.py you should call
+        :py:func`displayio.release_displays` first, otherwise it will error after the first
+        code.py run.
+        """
+        pass
+
+    def reset(self):
+        """Performs a hardware reset via the reset pin. Raises an exception if called
+        when no reset pin is available.
+        """
+        pass
+
+    def send(self, command, data):
+        """Sends the given command value followed by the full set of data. Display state,
+        such as vertical scroll, set via send may or may not be reset once the code is
+        done.
+        """
+        pass
+
+
+class OnDiskBitmap:
+    """
+    Loads values straight from disk. This minimizes memory use but can lead to much slower
+    pixel load times. These load times may result in frame tearing where only part of the
+    image is visible."""
+
+    def __init__(self, file):
+        self._image = Image.open(file)
+
+    @property
+    def width(self):
+        """Width of the bitmap. (read only)"""
+        return self._image.width
+
+    @property
+    def height(self):
+        """Height of the bitmap. (read only)"""
+        return self._image.height
+
+
+class Palette:
+    """Map a pixel palette_index to a full color. Colors are transformed to the display’s
+    format internally to save memory.
+    """
+
+    def __init__(self, color_count):
+        """Create a Palette object to store a set number of colors."""
+        self._needs_refresh = False
+
+        self._colors = []
+        for _ in range(color_count):
+            self._colors.append(self._make_color(0))
+
+    def _make_color(self, value, transparent=False):
+        color = {
+            "transparent": transparent,
+            "rgb888": 0,
+        }
+        if isinstance(value, (tuple, list, bytes, bytearray)):
+            value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
+        elif isinstance(value, int):
+            if not 0 <= value <= 0xFFFFFF:
+                raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
+        else:
+            raise TypeError("Color buffer must be a buffer, tuple, list, or int")
+        color["rgb888"] = value
+        self._needs_refresh = True
+
+        return color
+
+    def __len__(self):
+        """Returns the number of colors in a Palette"""
+        return len(self._colors)
+
+    def __setitem__(self, index, value):
+        """Sets the pixel color at the given index. The index should be
+        an integer in the range 0 to color_count-1.
+
+        The value argument represents a color, and can be from 0x000000 to 0xFFFFFF
+        (to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or
+        4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers.
+        """
+        if self._colors[index]["rgb888"] != value:
+            self._colors[index] = self._make_color(value)
+
+    def __getitem__(self, index):
+        if not 0 <= index < len(self._colors):
+            raise ValueError("Palette index out of range")
+        return self._colors[index]
+
+    def make_transparent(self, palette_index):
+        """Set the palette index to be a transparent color"""
+        self._colors[palette_index]["transparent"] = True
+
+    def make_opaque(self, palette_index):
+        """Set the palette index to be an opaque color"""
+        self._colors[palette_index]["transparent"] = False
+
+
+class ParallelBus:
+    """Manage updating a display over 8-bit parallel bus in the background while Python code
+    runs. This protocol may be refered to as 8080-I Series Parallel Interface in datasheets.
+    It doesn’t handle display initialization.
+    """
+
+    def __init__(self, i2c_bus, *, device_address, reset=None):
+        """Create a ParallelBus object associated with the given pins. The
+        bus is inferred from data0 by implying the next 7 additional pins on a given GPIO
+        port.
+
+        The parallel bus and pins are then in use by the display until
+        displayio.release_displays() is called even after a reload. (It does this so
+        CircuitPython can use the display after your code is done.) So, the first time you
+        initialize a display bus in code.py you should call
+        :py:func`displayio.release_displays` first, otherwise it will error after the first
+        code.py run.
+        """
+        pass
+
+    def reset(self):
+        """Performs a hardware reset via the reset pin. Raises an exception if called when
+        no reset pin is available.
+        """
+        pass
+
+    def send(self, command, data):
+        """Sends the given command value followed by the full set of data. Display state,
+        such as vertical scroll, set via ``send`` may or may not be reset once the code is
+        done.
+        """
+        pass
+
+
+class Shape(Bitmap):
+    """Create a Shape object with the given fixed size. Each pixel is one bit and is stored
+    by the column boundaries of the shape on each row. Each row’s boundary defaults to the
+    full row.
+    """
+
+    def __init__(self, width, height, *, mirror_x=False, mirror_y=False):
+        """Create a Shape object with the given fixed size. Each pixel is one bit and is
+        stored by the column boundaries of the shape on each row. Each row’s boundary
+        defaults to the full row.
+        """
+        super().__init__(width, height, 2)
+
+    def set_boundary(self, y, start_x, end_x):
+        """Loads pre-packed data into the given row."""
+        pass
+
+
+# pylint: disable=too-many-instance-attributes
+class TileGrid:
+    """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
+    grids can share bitmaps and pixel shaders.
+
+    A single tile grid is also known as a Sprite.
+    """
+
+    def __init__(
+        self,
+        bitmap,
+        *,
+        pixel_shader,
+        width=1,
+        height=1,
+        tile_width=None,
+        tile_height=None,
+        default_tile=0,
+        x=0,
+        y=0
+    ):
+        """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
+        used to convert the value and its location to a display native pixel color. This may
+        be a simple color palette lookup, a gradient, a pattern or a color transformer.
+
+        tile_width and tile_height match the height of the bitmap by default.
+        """
+        if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
+            raise ValueError("Unsupported Bitmap type")
+        self._bitmap = bitmap
+        bitmap_width = bitmap.width
+        bitmap_height = bitmap.height
+
+        if not isinstance(pixel_shader, (ColorConverter, Palette)):
+            raise ValueError("Unsupported Pixel Shader type")
+        self._pixel_shader = pixel_shader
+        self._hidden = False
+        self._x = x
+        self._y = y
+        self._width = width  # Number of Tiles Wide
+        self._height = height  # Number of Tiles High
+        self._transpose_xy = False
+        self._flip_x = False
+        self._flip_y = False
+        if tile_width is None:
+            tile_width = bitmap_width
+        if tile_height is None:
+            tile_height = bitmap_height
+        if bitmap_width % tile_width != 0:
+            raise ValueError("Tile width must exactly divide bitmap width")
+        self._tile_width = tile_width
+        if bitmap_height % tile_height != 0:
+            raise ValueError("Tile height must exactly divide bitmap height")
+        self._tile_height = tile_height
+        if not 0 <= default_tile <= 255:
+            raise ValueError("Default Tile is out of range")
+        self._pixel_width = width * tile_width
+        self._pixel_height = height * tile_height
+        self._tiles = (self._width * self._height) * [default_tile]
+        self.in_group = False
+        self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
+        self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
+        self._moved = False
+
+    def update_transform(self, absolute_transform):
+        """Update the parent transform and child transforms"""
+        self._absolute_transform = absolute_transform
+        if self._absolute_transform is not None:
+            self._update_current_x()
+            self._update_current_y()
+
+    def _update_current_x(self):
+        if self._transpose_xy:
+            width = self._pixel_height
+        else:
+            width = self._pixel_width
+        if self._absolute_transform.transpose_xy:
+            self._current_area.y1 = (
+                self._absolute_transform.y + self._absolute_transform.dy * self._x
+            )
+            self._current_area.y2 = (
+                self._absolute_transform.y
+                + self._absolute_transform.dy * (self._x + width)
+            )
+            if self._current_area.y2 < self._current_area.y1:
+                self._current_area.y1, self._current_area.y2 = (
+                    self._current_area.y2,
+                    self._current_area.y1,
+                )
+        else:
+            self._current_area.x1 = (
+                self._absolute_transform.x + self._absolute_transform.dx * self._x
+            )
+            self._current_area.x2 = (
+                self._absolute_transform.x
+                + self._absolute_transform.dx * (self._x + width)
+            )
+            if self._current_area.x2 < self._current_area.x1:
+                self._current_area.x1, self._current_area.x2 = (
+                    self._current_area.x2,
+                    self._current_area.x1,
+                )
+
+    def _update_current_y(self):
+        if self._transpose_xy:
+            height = self._pixel_width
+        else:
+            height = self._pixel_height
+        if self._absolute_transform.transpose_xy:
+            self._current_area.x1 = (
+                self._absolute_transform.x + self._absolute_transform.dx * self._y
+            )
+            self._current_area.x2 = (
+                self._absolute_transform.x
+                + self._absolute_transform.dx * (self._y + height)
+            )
+            if self._current_area.x2 < self._current_area.x1:
+                self._current_area.x1, self._current_area.x2 = (
+                    self._current_area.x2,
+                    self._current_area.x1,
+                )
+        else:
+            self._current_area.y1 = (
+                self._absolute_transform.y + self._absolute_transform.dy * self._y
+            )
+            self._current_area.y2 = (
+                self._absolute_transform.y
+                + self._absolute_transform.dy * (self._y + height)
+            )
+            if self._current_area.y2 < self._current_area.y1:
+                self._current_area.y1, self._current_area.y2 = (
+                    self._current_area.y2,
+                    self._current_area.y1,
+                )
+
+    # pylint: disable=too-many-locals
+    def _fill_area(self, buffer):
+        """Draw onto the image"""
+        if self._hidden:
+            return
+
+        image = Image.new(
+            "RGBA",
+            (self._width * self._tile_width, self._height * self._tile_height),
+            (0, 0, 0, 0),
+        )
+
+        tile_count_x = self._bitmap.width // self._tile_width
+        x = self._x
+        y = self._y
+
+        for tile_x in range(0, self._width):
+            for tile_y in range(0, self._height):
+                tile_index = self._tiles[tile_y * self._width + tile_x]
+                tile_index_x = tile_index % tile_count_x
+                tile_index_y = tile_index // tile_count_x
+                for pixel_x in range(self._tile_width):
+                    for pixel_y in range(self._tile_height):
+                        image_x = tile_x * self._tile_width + pixel_x
+                        image_y = tile_y * self._tile_height + pixel_y
+                        bitmap_x = tile_index_x * self._tile_width + pixel_x
+                        bitmap_y = tile_index_y * self._tile_height + pixel_y
+                        pixel_color = self._pixel_shader[
+                            self._bitmap[bitmap_x, bitmap_y]
+                        ]
+                        if not pixel_color["transparent"]:
+                            image.putpixel((image_x, image_y), pixel_color["rgb888"])
+        if self._absolute_transform is not None:
+            if self._absolute_transform.scale > 1:
+                image = image.resize(
+                    (
+                        self._pixel_width * self._absolute_transform.scale,
+                        self._pixel_height * self._absolute_transform.scale,
+                    ),
+                    resample=Image.NEAREST,
+                )
+            if self._absolute_transform.mirror_x:
+                image = image.transpose(Image.FLIP_LEFT_RIGHT)
+            if self._absolute_transform.mirror_y:
+                image = image.transpose(Image.FLIP_TOP_BOTTOM)
+            if self._absolute_transform.transpose_xy:
+                image = image.transpose(Image.TRANSPOSE)
+            x *= self._absolute_transform.dx
+            y *= self._absolute_transform.dy
+            x += self._absolute_transform.x
+            y += self._absolute_transform.y
+        buffer.alpha_composite(image, (x, y))
+
+    # pylint: enable=too-many-locals
+
+    @property
+    def hidden(self):
+        """True when the TileGrid is hidden. This may be False even
+        when a part of a hidden Group."""
+        return self._hidden
+
+    @hidden.setter
+    def hidden(self, value):
+        if not isinstance(value, (bool, int)):
+            raise ValueError("Expecting a boolean or integer value")
+        self._hidden = bool(value)
+
+    @property
+    def x(self):
+        """X position of the left edge in the parent."""
+        return self._x
+
+    @x.setter
+    def x(self, value):
+        if not isinstance(value, int):
+            raise TypeError("X should be a integer type")
+        if self._x != value:
+            self._x = value
+            self._update_current_x()
+
+    @property
+    def y(self):
+        """Y position of the top edge in the parent."""
+        return self._y
+
+    @y.setter
+    def y(self, value):
+        if not isinstance(value, int):
+            raise TypeError("Y should be a integer type")
+        if self._y != value:
+            self._y = value
+            self._update_current_y()
+
+    @property
+    def flip_x(self):
+        """If true, the left edge rendered will be the right edge of the right-most tile."""
+        return self._flip_x
+
+    @flip_x.setter
+    def flip_x(self, value):
+        if not isinstance(value, bool):
+            raise TypeError("Flip X should be a boolean type")
+        if self._flip_x != value:
+            self._flip_x = value
+
+    @property
+    def flip_y(self):
+        """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
+        return self._flip_y
+
+    @flip_y.setter
+    def flip_y(self, value):
+        if not isinstance(value, bool):
+            raise TypeError("Flip Y should be a boolean type")
+        if self._flip_y != value:
+            self._flip_y = value
+
+    @property
+    def transpose_xy(self):
+        """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
+        degree rotation can be achieved along with the corresponding mirrored version.
+        """
+        return self._transpose_xy
+
+    @transpose_xy.setter
+    def transpose_xy(self, value):
+        if not isinstance(value, bool):
+            raise TypeError("Transpose XY should be a boolean type")
+        if self._transpose_xy != value:
+            self._transpose_xy = value
+            self._update_current_x()
+            self._update_current_y()
+
+    @property
+    def pixel_shader(self):
+        """The pixel shader of the tilegrid."""
+        pass
+
+    def __getitem__(self, index):
+        """Returns the tile index at the given index. The index can either be
+        an x,y tuple or an int equal to ``y * width + x``'.
+        """
+        if isinstance(index, (tuple, list)):
+            x = index[0]
+            y = index[1]
+            index = y * self._width + x
+        elif isinstance(index, int):
+            x = index % self._width
+            y = index // self._width
+        if x > self._width or y > self._height or index >= len(self._tiles):
+            raise ValueError("Tile index out of bounds")
+        return self._tiles[index]
+
+    def __setitem__(self, index, value):
+        """Sets the tile index at the given index. The index can either be
+        an x,y tuple or an int equal to ``y * width + x``.
+        """
+        if isinstance(index, (tuple, list)):
+            x = index[0]
+            y = index[1]
+            index = y * self._width + x
+        elif isinstance(index, int):
+            x = index % self._width
+            y = index // self._width
+        if x > self._width or y > self._height or index >= len(self._tiles):
+            raise ValueError("Tile index out of bounds")
+        if not 0 <= value <= 255:
+            raise ValueError("Tile value out of bounds")
+        self._tiles[index] = value
+
+
+# pylint: enable=too-many-instance-attributes
diff --git a/displayio/fourwire.py b/displayio/fourwire.py
new file mode 100644 (file)
index 0000000..5d8a433
--- /dev/null
@@ -0,0 +1,122 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+`displayio.fourwire`
+================================================================================
+
+displayio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+import time
+import digitalio
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
+
+class FourWire:
+    """Manage updating a display over SPI four wire protocol in the background while
+    Python code runs. It doesn’t handle display initialization.
+    """
+
+    def __init__(
+        self,
+        spi_bus,
+        *,
+        command,
+        chip_select,
+        reset=None,
+        baudrate=24000000,
+        polarity=0,
+        phase=0
+    ):
+        """Create a FourWire object associated with the given pins.
+
+        The SPI bus and pins are then in use by the display until
+        displayio.release_displays() is called even after a reload. (It does this so
+        CircuitPython can use the display after your code is done.)
+        So, the first time you initialize a display bus in code.py you should call
+        :py:func`displayio.release_displays` first, otherwise it will error after the
+        first code.py run.
+        """
+        self._dc = digitalio.DigitalInOut(command)
+        self._dc.switch_to_output()
+        self._chip_select = digitalio.DigitalInOut(chip_select)
+        self._chip_select.switch_to_output(value=True)
+
+        if reset is not None:
+            self._reset = digitalio.DigitalInOut(reset)
+            self._reset.switch_to_output(value=True)
+        else:
+            self._reset = None
+        self._spi = spi_bus
+        while self._spi.try_lock():
+            pass
+        self._spi.configure(baudrate=baudrate, polarity=polarity, phase=phase)
+        self._spi.unlock()
+
+    def _release(self):
+        self.reset()
+        self._spi.deinit()
+        self._dc.deinit()
+        self._chip_select.deinit()
+        if self._reset is not None:
+            self._reset.deinit()
+
+    def reset(self):
+        """Performs a hardware reset via the reset pin.
+        Raises an exception if called when no reset pin is available.
+        """
+        if self._reset is not None:
+            self._reset.value = False
+            time.sleep(0.001)
+            self._reset.value = True
+            time.sleep(0.001)
+        else:
+            raise RuntimeError("No reset pin defined")
+
+    def send(self, is_command, data, *, toggle_every_byte=False):
+        """Sends the given command value followed by the full set of data. Display state,
+        such as vertical scroll, set via ``send`` may or may not be reset once the code is
+        done.
+        """
+        while self._spi.try_lock():
+            pass
+        self._dc.value = not is_command
+        if toggle_every_byte:
+            for byte in data:
+                self._spi.write(bytes([byte]))
+                self._chip_select.value = True
+                time.sleep(0.000001)
+                self._chip_select.value = False
+        else:
+            self._spi.write(data)
+        self._spi.unlock()
diff --git a/displayio/group.py b/displayio/group.py
new file mode 100644 (file)
index 0000000..d28d83b
--- /dev/null
@@ -0,0 +1,235 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+`displayio.group`
+================================================================================
+
+displayio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+from recordclass import recordclass
+from displayio.tilegrid import TileGrid
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
+
+Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
+
+
+class Group:
+    """Manage a group of sprites and groups and how they are inter-related."""
+
+    def __init__(self, *, max_size=4, scale=1, x=0, y=0):
+        """Create a Group of a given size and scale. Scale is in
+        one dimension. For example, scale=2 leads to a layer’s
+        pixel being 2x2 pixels when in the group.
+        """
+        if not isinstance(max_size, int) or max_size < 1:
+            raise ValueError("Max Size must be >= 1")
+        self._max_size = max_size
+        if not isinstance(scale, int) or scale < 1:
+            raise ValueError("Scale must be >= 1")
+        self._scale = scale
+        self._x = x
+        self._y = y
+        self._hidden = False
+        self._layers = []
+        self._supported_types = (TileGrid, Group)
+        self._absolute_transform = None
+        self.in_group = False
+        self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
+
+    def update_transform(self, parent_transform):
+        """Update the parent transform and child transforms"""
+        self.in_group = parent_transform is not None
+        if self.in_group:
+            x = self._x
+            y = self._y
+            if parent_transform.transpose_xy:
+                x, y = y, x
+            self._absolute_transform.x = parent_transform.x + parent_transform.dx * x
+            self._absolute_transform.y = parent_transform.y + parent_transform.dy * y
+            self._absolute_transform.dx = parent_transform.dx * self._scale
+            self._absolute_transform.dy = parent_transform.dy * self._scale
+            self._absolute_transform.transpose_xy = parent_transform.transpose_xy
+            self._absolute_transform.mirror_x = parent_transform.mirror_x
+            self._absolute_transform.mirror_y = parent_transform.mirror_y
+            self._absolute_transform.scale = parent_transform.scale * self._scale
+        self._update_child_transforms()
+
+    def _update_child_transforms(self):
+        if self.in_group:
+            for layer in self._layers:
+                layer.update_transform(self._absolute_transform)
+
+    def _removal_cleanup(self, index):
+        layer = self._layers[index]
+        layer.update_transform(None)
+
+    def _layer_update(self, index):
+        layer = self._layers[index]
+        layer.update_transform(self._absolute_transform)
+
+    def append(self, layer):
+        """Append a layer to the group. It will be drawn
+        above other layers.
+        """
+        self.insert(len(self._layers), layer)
+
+    def insert(self, index, layer):
+        """Insert a layer into the group."""
+        if not isinstance(layer, self._supported_types):
+            raise ValueError("Invalid Group Member")
+        if layer.in_group:
+            raise ValueError("Layer already in a group.")
+        if len(self._layers) == self._max_size:
+            raise RuntimeError("Group full")
+        self._layers.insert(index, layer)
+        self._layer_update(index)
+
+    def index(self, layer):
+        """Returns the index of the first copy of layer.
+        Raises ValueError if not found.
+        """
+        return self._layers.index(layer)
+
+    def pop(self, index=-1):
+        """Remove the ith item and return it."""
+        self._removal_cleanup(index)
+        return self._layers.pop(index)
+
+    def remove(self, layer):
+        """Remove the first copy of layer. Raises ValueError
+        if it is not present."""
+        index = self.index(layer)
+        self._layers.pop(index)
+
+    def __len__(self):
+        """Returns the number of layers in a Group"""
+        return len(self._layers)
+
+    def __getitem__(self, index):
+        """Returns the value at the given index."""
+        return self._layers[index]
+
+    def __setitem__(self, index, value):
+        """Sets the value at the given index."""
+        self._removal_cleanup(index)
+        self._layers[index] = value
+        self._layer_update(index)
+
+    def __delitem__(self, index):
+        """Deletes the value at the given index."""
+        del self._layers[index]
+
+    def _fill_area(self, buffer):
+        if self._hidden:
+            return
+
+        for layer in self._layers:
+            if isinstance(layer, (Group, TileGrid)):
+                layer._fill_area(buffer)  # pylint: disable=protected-access
+
+    @property
+    def hidden(self):
+        """True when the Group and all of it’s layers are not visible. When False, the
+        Group’s layers are visible if they haven’t been hidden.
+        """
+        return self._hidden
+
+    @hidden.setter
+    def hidden(self, value):
+        if not isinstance(value, (bool, int)):
+            raise ValueError("Expecting a boolean or integer value")
+        self._hidden = bool(value)
+
+    @property
+    def scale(self):
+        """Scales each pixel within the Group in both directions. For example, when
+        scale=2 each pixel will be represented by 2x2 pixels.
+        """
+        return self._scale
+
+    @scale.setter
+    def scale(self, value):
+        if not isinstance(value, int) or value < 1:
+            raise ValueError("Scale must be >= 1")
+        if self._scale != value:
+            parent_scale = self._absolute_transform.scale / self._scale
+            self._absolute_transform.dx = (
+                self._absolute_transform.dx / self._scale * value
+            )
+            self._absolute_transform.dy = (
+                self._absolute_transform.dy / self._scale * value
+            )
+            self._absolute_transform.scale = parent_scale * value
+
+            self._scale = value
+            self._update_child_transforms()
+
+    @property
+    def x(self):
+        """X position of the Group in the parent."""
+        return self._x
+
+    @x.setter
+    def x(self, value):
+        if not isinstance(value, int):
+            raise ValueError("x must be an integer")
+        if self._x != value:
+            if self._absolute_transform.transpose_xy:
+                dy_value = self._absolute_transform.dy / self._scale
+                self._absolute_transform.y += dy_value * (value - self._x)
+            else:
+                dx_value = self._absolute_transform.dx / self._scale
+                self._absolute_transform.x += dx_value * (value - self._x)
+            self._x = value
+            self._update_child_transforms()
+
+    @property
+    def y(self):
+        """Y position of the Group in the parent."""
+        return self._y
+
+    @y.setter
+    def y(self, value):
+        if not isinstance(value, int):
+            raise ValueError("y must be an integer")
+        if self._y != value:
+            if self._absolute_transform.transpose_xy:
+                dx_value = self._absolute_transform.dx / self._scale
+                self._absolute_transform.x += dx_value * (value - self._y)
+            else:
+                dy_value = self._absolute_transform.dy / self._scale
+                self._absolute_transform.y += dy_value * (value - self._y)
+            self._y = value
+            self._update_child_transforms()
diff --git a/displayio/i2cdisplay.py b/displayio/i2cdisplay.py
new file mode 100644 (file)
index 0000000..21fa371
--- /dev/null
@@ -0,0 +1,71 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+`displayio.i2cdisplay`
+================================================================================
+
+displayio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
+# pylint: disable=unnecessary-pass, unused-argument
+
+
+class I2CDisplay:
+    """Manage updating a display over I2C in the background while Python code runs.
+    It doesn’t handle display initialization.
+    """
+
+    def __init__(self, i2c_bus, *, device_address, reset=None):
+        """Create a I2CDisplay object associated with the given I2C bus and reset pin.
+
+        The I2C bus and pins are then in use by the display until displayio.release_displays() is
+        called even after a reload. (It does this so CircuitPython can use the display after your
+        code is done.) So, the first time you initialize a display bus in code.py you should call
+        :py:func`displayio.release_displays` first, otherwise it will error after the first
+        code.py run.
+        """
+        pass
+
+    def reset(self):
+        """Performs a hardware reset via the reset pin. Raises an exception if called
+        when no reset pin is available.
+        """
+        pass
+
+    def send(self, command, data):
+        """Sends the given command value followed by the full set of data. Display state,
+        such as vertical scroll, set via send may or may not be reset once the code is
+        done.
+        """
+        pass
diff --git a/displayio/ondiskbitmap.py b/displayio/ondiskbitmap.py
new file mode 100644 (file)
index 0000000..3a08ab3
--- /dev/null
@@ -0,0 +1,63 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+`displayio.ondiskbitmap`
+================================================================================
+
+displayio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+from PIL import Image
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
+# pylint: disable=unnecessary-pass, unused-argument
+
+
+class OnDiskBitmap:
+    """
+    Loads values straight from disk. This minimizes memory use but can lead to much slower
+    pixel load times. These load times may result in frame tearing where only part of the
+    image is visible."""
+
+    def __init__(self, file):
+        self._image = Image.open(file)
+
+    @property
+    def width(self):
+        """Width of the bitmap. (read only)"""
+        return self._image.width
+
+    @property
+    def height(self):
+        """Height of the bitmap. (read only)"""
+        return self._image.height
diff --git a/displayio/palette.py b/displayio/palette.py
new file mode 100644 (file)
index 0000000..1812dbf
--- /dev/null
@@ -0,0 +1,113 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+`displayio.palette`
+================================================================================
+
+displayio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
+
+class Palette:
+    """Map a pixel palette_index to a full color. Colors are transformed to the display’s
+    format internally to save memory.
+    """
+
+    def __init__(self, color_count):
+        """Create a Palette object to store a set number of colors."""
+        self._needs_refresh = False
+
+        self._colors = []
+        for _ in range(color_count):
+            self._colors.append(self._make_color(0))
+            self._update_rgba(len(self._colors) - 1)
+
+    def _update_rgba(self, index):
+        color = self._colors[index]["rgb888"]
+        transparent = self._colors[index]["transparent"]
+        self._colors[index]["rgba"] = (
+            color >> 16,
+            (color >> 8) & 0xFF,
+            color & 0xFF,
+            0 if transparent else 0xFF,
+        )
+
+    def _make_color(self, value, transparent=False):
+        color = {
+            "transparent": transparent,
+            "rgb888": 0,
+            "rgba": (0, 0, 0, 255),
+        }
+        if isinstance(value, (tuple, list, bytes, bytearray)):
+            value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
+        elif isinstance(value, int):
+            if not 0 <= value <= 0xFFFFFF:
+                raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
+        else:
+            raise TypeError("Color buffer must be a buffer, tuple, list, or int")
+        color["rgb888"] = value
+        self._needs_refresh = True
+
+        return color
+
+    def __len__(self):
+        """Returns the number of colors in a Palette"""
+        return len(self._colors)
+
+    def __setitem__(self, index, value):
+        """Sets the pixel color at the given index. The index should be
+        an integer in the range 0 to color_count-1.
+
+        The value argument represents a color, and can be from 0x000000 to 0xFFFFFF
+        (to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or
+        4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers.
+        """
+        if self._colors[index]["rgb888"] != value:
+            self._colors[index] = self._make_color(value)
+            self._update_rgba(index)
+
+    def __getitem__(self, index):
+        if not 0 <= index < len(self._colors):
+            raise ValueError("Palette index out of range")
+        return self._colors[index]
+
+    def make_transparent(self, palette_index):
+        """Set the palette index to be a transparent color"""
+        self._colors[palette_index]["transparent"] = True
+        self._update_rgba(palette_index)
+
+    def make_opaque(self, palette_index):
+        """Set the palette index to be an opaque color"""
+        self._colors[palette_index]["transparent"] = False
+        self._update_rgba(palette_index)
diff --git a/displayio/parallelbus.py b/displayio/parallelbus.py
new file mode 100644 (file)
index 0000000..f6e75d6
--- /dev/null
@@ -0,0 +1,75 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+`displayio.parallelbus`
+================================================================================
+
+displayio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
+# pylint: disable=unnecessary-pass, unused-argument
+
+
+class ParallelBus:
+    """Manage updating a display over 8-bit parallel bus in the background while Python code
+    runs. This protocol may be refered to as 8080-I Series Parallel Interface in datasheets.
+    It doesn’t handle display initialization.
+    """
+
+    def __init__(self, i2c_bus, *, device_address, reset=None):
+        """Create a ParallelBus object associated with the given pins. The
+        bus is inferred from data0 by implying the next 7 additional pins on a given GPIO
+        port.
+
+        The parallel bus and pins are then in use by the display until
+        displayio.release_displays() is called even after a reload. (It does this so
+        CircuitPython can use the display after your code is done.) So, the first time you
+        initialize a display bus in code.py you should call
+        :py:func`displayio.release_displays` first, otherwise it will error after the first
+        code.py run.
+        """
+        pass
+
+    def reset(self):
+        """Performs a hardware reset via the reset pin. Raises an exception if called when
+        no reset pin is available.
+        """
+        pass
+
+    def send(self, command, data):
+        """Sends the given command value followed by the full set of data. Display state,
+        such as vertical scroll, set via ``send`` may or may not be reset once the code is
+        done.
+        """
+        pass
diff --git a/displayio/shape.py b/displayio/shape.py
new file mode 100644 (file)
index 0000000..36f0c04
--- /dev/null
@@ -0,0 +1,61 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+`displayio.shape`
+================================================================================
+
+displayio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+from displayio.bitmap import Bitmap
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
+# pylint: disable=unnecessary-pass, unused-argument
+
+
+class Shape(Bitmap):
+    """Create a Shape object with the given fixed size. Each pixel is one bit and is stored
+    by the column boundaries of the shape on each row. Each row’s boundary defaults to the
+    full row.
+    """
+
+    def __init__(self, width, height, *, mirror_x=False, mirror_y=False):
+        """Create a Shape object with the given fixed size. Each pixel is one bit and is
+        stored by the column boundaries of the shape on each row. Each row’s boundary
+        defaults to the full row.
+        """
+        super().__init__(width, height, 2)
+
+    def set_boundary(self, y, start_x, end_x):
+        """Loads pre-packed data into the given row."""
+        pass
diff --git a/displayio/test.png b/displayio/test.png
new file mode 100644 (file)
index 0000000..dca5f07
Binary files /dev/null and b/displayio/test.png differ
diff --git a/displayio/tilegrid.py b/displayio/tilegrid.py
new file mode 100644 (file)
index 0000000..76d3b2c
--- /dev/null
@@ -0,0 +1,359 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+`displayio.tilegrid`
+================================================================================
+
+displayio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+from recordclass import recordclass
+from PIL import Image
+from displayio.bitmap import Bitmap
+from displayio.colorconverter import ColorConverter
+from displayio.ondiskbitmap import OnDiskBitmap
+from displayio.shape import Shape
+from displayio.palette import Palette
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
+Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
+Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
+
+# pylint: disable=too-many-instance-attributes
+class TileGrid:
+    """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
+    grids can share bitmaps and pixel shaders.
+
+    A single tile grid is also known as a Sprite.
+    """
+
+    def __init__(
+        self,
+        bitmap,
+        *,
+        pixel_shader,
+        width=1,
+        height=1,
+        tile_width=None,
+        tile_height=None,
+        default_tile=0,
+        x=0,
+        y=0
+    ):
+        """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
+        used to convert the value and its location to a display native pixel color. This may
+        be a simple color palette lookup, a gradient, a pattern or a color transformer.
+
+        tile_width and tile_height match the height of the bitmap by default.
+        """
+        if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
+            raise ValueError("Unsupported Bitmap type")
+        self._bitmap = bitmap
+        bitmap_width = bitmap.width
+        bitmap_height = bitmap.height
+
+        if not isinstance(pixel_shader, (ColorConverter, Palette)):
+            raise ValueError("Unsupported Pixel Shader type")
+        self._pixel_shader = pixel_shader
+        self._hidden = False
+        self._x = x
+        self._y = y
+        self._width = width  # Number of Tiles Wide
+        self._height = height  # Number of Tiles High
+        self._transpose_xy = False
+        self._flip_x = False
+        self._flip_y = False
+        if tile_width is None:
+            tile_width = bitmap_width
+        if tile_height is None:
+            tile_height = bitmap_height
+        if bitmap_width % tile_width != 0:
+            raise ValueError("Tile width must exactly divide bitmap width")
+        self._tile_width = tile_width
+        if bitmap_height % tile_height != 0:
+            raise ValueError("Tile height must exactly divide bitmap height")
+        self._tile_height = tile_height
+        if not 0 <= default_tile <= 255:
+            raise ValueError("Default Tile is out of range")
+        self._pixel_width = width * tile_width
+        self._pixel_height = height * tile_height
+        self._tiles = (self._width * self._height) * [default_tile]
+        self.in_group = False
+        self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
+        self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
+        self._moved = False
+
+    def update_transform(self, absolute_transform):
+        """Update the parent transform and child transforms"""
+        self._absolute_transform = absolute_transform
+        if self._absolute_transform is not None:
+            self._update_current_x()
+            self._update_current_y()
+
+    def _update_current_x(self):
+        if self._transpose_xy:
+            width = self._pixel_height
+        else:
+            width = self._pixel_width
+        if self._absolute_transform.transpose_xy:
+            self._current_area.y1 = (
+                self._absolute_transform.y + self._absolute_transform.dy * self._x
+            )
+            self._current_area.y2 = (
+                self._absolute_transform.y
+                + self._absolute_transform.dy * (self._x + width)
+            )
+            if self._current_area.y2 < self._current_area.y1:
+                self._current_area.y1, self._current_area.y2 = (
+                    self._current_area.y2,
+                    self._current_area.y1,
+                )
+        else:
+            self._current_area.x1 = (
+                self._absolute_transform.x + self._absolute_transform.dx * self._x
+            )
+            self._current_area.x2 = (
+                self._absolute_transform.x
+                + self._absolute_transform.dx * (self._x + width)
+            )
+            if self._current_area.x2 < self._current_area.x1:
+                self._current_area.x1, self._current_area.x2 = (
+                    self._current_area.x2,
+                    self._current_area.x1,
+                )
+
+    def _update_current_y(self):
+        if self._transpose_xy:
+            height = self._pixel_width
+        else:
+            height = self._pixel_height
+        if self._absolute_transform.transpose_xy:
+            self._current_area.x1 = (
+                self._absolute_transform.x + self._absolute_transform.dx * self._y
+            )
+            self._current_area.x2 = (
+                self._absolute_transform.x
+                + self._absolute_transform.dx * (self._y + height)
+            )
+            if self._current_area.x2 < self._current_area.x1:
+                self._current_area.x1, self._current_area.x2 = (
+                    self._current_area.x2,
+                    self._current_area.x1,
+                )
+        else:
+            self._current_area.y1 = (
+                self._absolute_transform.y + self._absolute_transform.dy * self._y
+            )
+            self._current_area.y2 = (
+                self._absolute_transform.y
+                + self._absolute_transform.dy * (self._y + height)
+            )
+            if self._current_area.y2 < self._current_area.y1:
+                self._current_area.y1, self._current_area.y2 = (
+                    self._current_area.y2,
+                    self._current_area.y1,
+                )
+
+    # pylint: disable=too-many-locals
+    def _fill_area(self, buffer):
+        """Draw onto the image"""
+        if self._hidden:
+            return
+
+        image = Image.new(
+            "RGBA",
+            (self._width * self._tile_width, self._height * self._tile_height),
+            (0, 0, 0, 0),
+        )
+
+        tile_count_x = self._bitmap.width // self._tile_width
+        x = self._x
+        y = self._y
+
+        for tile_x in range(self._width):
+            for tile_y in range(self._height):
+                tile_index = self._tiles[tile_y * self._width + tile_x]
+                tile_index_x = tile_index % tile_count_x
+                tile_index_y = tile_index // tile_count_x
+                for pixel_x in range(self._tile_width):
+                    for pixel_y in range(self._tile_height):
+                        image_x = (tile_x * self._tile_width) + pixel_x
+                        image_y = (tile_y * self._tile_height) + pixel_y
+                        bitmap_x = (tile_index_x * self._tile_width) + pixel_x
+                        bitmap_y = (tile_index_y * self._tile_height) + pixel_y
+                        pixel_color = self._pixel_shader[
+                            self._bitmap[bitmap_x, bitmap_y]
+                        ]
+                        # if not pixel_color["transparent"]:
+                        image.putpixel((image_x, image_y), pixel_color["rgba"])
+
+        if self._absolute_transform is not None:
+            if self._absolute_transform.scale > 1:
+                image = image.resize(
+                    (
+                        self._pixel_width * self._absolute_transform.scale,
+                        self._pixel_height * self._absolute_transform.scale,
+                    ),
+                    resample=Image.NEAREST,
+                )
+            if self._absolute_transform.mirror_x:
+                image = image.transpose(Image.FLIP_LEFT_RIGHT)
+            if self._absolute_transform.mirror_y:
+                image = image.transpose(Image.FLIP_TOP_BOTTOM)
+            if self._absolute_transform.transpose_xy:
+                image = image.transpose(Image.TRANSPOSE)
+            x *= self._absolute_transform.dx
+            y *= self._absolute_transform.dy
+            x += self._absolute_transform.x
+            y += self._absolute_transform.y
+        buffer.alpha_composite(image, (x, y))
+
+    # pylint: enable=too-many-locals
+
+    @property
+    def hidden(self):
+        """True when the TileGrid is hidden. This may be False even
+        when a part of a hidden Group."""
+        return self._hidden
+
+    @hidden.setter
+    def hidden(self, value):
+        if not isinstance(value, (bool, int)):
+            raise ValueError("Expecting a boolean or integer value")
+        self._hidden = bool(value)
+
+    @property
+    def x(self):
+        """X position of the left edge in the parent."""
+        return self._x
+
+    @x.setter
+    def x(self, value):
+        if not isinstance(value, int):
+            raise TypeError("X should be a integer type")
+        if self._x != value:
+            self._x = value
+            self._update_current_x()
+
+    @property
+    def y(self):
+        """Y position of the top edge in the parent."""
+        return self._y
+
+    @y.setter
+    def y(self, value):
+        if not isinstance(value, int):
+            raise TypeError("Y should be a integer type")
+        if self._y != value:
+            self._y = value
+            self._update_current_y()
+
+    @property
+    def flip_x(self):
+        """If true, the left edge rendered will be the right edge of the right-most tile."""
+        return self._flip_x
+
+    @flip_x.setter
+    def flip_x(self, value):
+        if not isinstance(value, bool):
+            raise TypeError("Flip X should be a boolean type")
+        if self._flip_x != value:
+            self._flip_x = value
+
+    @property
+    def flip_y(self):
+        """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
+        return self._flip_y
+
+    @flip_y.setter
+    def flip_y(self, value):
+        if not isinstance(value, bool):
+            raise TypeError("Flip Y should be a boolean type")
+        if self._flip_y != value:
+            self._flip_y = value
+
+    @property
+    def transpose_xy(self):
+        """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
+        degree rotation can be achieved along with the corresponding mirrored version.
+        """
+        return self._transpose_xy
+
+    @transpose_xy.setter
+    def transpose_xy(self, value):
+        if not isinstance(value, bool):
+            raise TypeError("Transpose XY should be a boolean type")
+        if self._transpose_xy != value:
+            self._transpose_xy = value
+            self._update_current_x()
+            self._update_current_y()
+
+    @property
+    def pixel_shader(self):
+        """The pixel shader of the tilegrid."""
+        return self._pixel_shader
+
+    def __getitem__(self, index):
+        """Returns the tile index at the given index. The index can either be
+        an x,y tuple or an int equal to ``y * width + x``'.
+        """
+        if isinstance(index, (tuple, list)):
+            x = index[0]
+            y = index[1]
+            index = y * self._width + x
+        elif isinstance(index, int):
+            x = index % self._width
+            y = index // self._width
+        if x > self._width or y > self._height or index >= len(self._tiles):
+            raise ValueError("Tile index out of bounds")
+        return self._tiles[index]
+
+    def __setitem__(self, index, value):
+        """Sets the tile index at the given index. The index can either be
+        an x,y tuple or an int equal to ``y * width + x``.
+        """
+        if isinstance(index, (tuple, list)):
+            x = index[0]
+            y = index[1]
+            index = y * self._width + x
+        elif isinstance(index, int):
+            x = index % self._width
+            y = index // self._width
+        if x > self._width or y > self._height or index >= len(self._tiles):
+            raise ValueError("Tile index out of bounds")
+        if not 0 <= value <= 255:
+            raise ValueError("Tile value out of bounds")
+        self._tiles[index] = value
+
+
+# pylint: enable=too-many-instance-attributes
diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico
new file mode 100644 (file)
index 0000000..5aca983
Binary files /dev/null and b/docs/_static/favicon.ico differ
diff --git a/docs/api.rst b/docs/api.rst
new file mode 100644 (file)
index 0000000..2fa7117
--- /dev/null
@@ -0,0 +1,14 @@
+
+.. If you created a package, create one automodule per module in the package.
+
+.. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py)
+.. use this format as the module name: "adafruit_foo.foo"
+
+.. automodule:: displayio
+   :members:
+
+.. automodule:: fontio
+   :members:
+
+.. automodule:: terminalio
+   :members:
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644 (file)
index 0000000..5528ca6
--- /dev/null
@@ -0,0 +1,180 @@
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+
+sys.path.insert(0, os.path.abspath(".."))
+
+# -- General configuration ------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+    "sphinx.ext.autodoc",
+    "sphinx.ext.intersphinx",
+    "sphinx.ext.napoleon",
+    "sphinx.ext.todo",
+]
+
+# TODO: Please Read!
+# Uncomment the below if you use native CircuitPython modules such as
+# digitalio, micropython and busio. List the modules you use. Without it, the
+# autodoc module docs will fail to generate with a warning.
+# autodoc_mock_imports = ["digitalio", "busio"]
+
+
+intersphinx_mapping = {
+    "python": ("https://docs.python.org/3.4", None),
+    "CircuitPython": ("https://circuitpython.readthedocs.io/en/latest/", None),
+}
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+source_suffix = ".rst"
+
+# The master toctree document.
+master_doc = "index"
+
+# General information about the project.
+project = "Adafruit_blinka displayio Library"
+copyright = "2020 Melissa LeBlanc-Williams"
+author = "Melissa LeBlanc-Williams"
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = "1.0"
+# The full version, including alpha/beta/rc tags.
+release = "1.0"
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = [
+    "_build",
+    "Thumbs.db",
+    ".DS_Store",
+    ".env",
+    "CODE_OF_CONDUCT.md",
+]
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#
+default_role = "any"
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#
+add_function_parentheses = True
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = "sphinx"
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+# If this is True, todo emits a warning for each TODO entries. The default is False.
+todo_emit_warnings = True
+
+napoleon_numpy_docstring = False
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+on_rtd = os.environ.get("READTHEDOCS", None) == "True"
+
+if not on_rtd:  # only import and set the theme if we're building docs locally
+    try:
+        import sphinx_rtd_theme
+
+        html_theme = "sphinx_rtd_theme"
+        html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."]
+    except:
+        html_theme = "default"
+        html_theme_path = ["."]
+else:
+    html_theme_path = ["."]
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ["_static"]
+
+# The name of an image file (relative to this directory) to use as a favicon of
+# the docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#
+html_favicon = "_static/favicon.ico"
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = "Adafruit_blinkaDisplayioLibrarydoc"
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+    # The paper size ('letterpaper' or 'a4paper').
+    # 'papersize': 'letterpaper',
+    # The font size ('10pt', '11pt' or '12pt').
+    # 'pointsize': '10pt',
+    # Additional stuff for the LaTeX preamble.
+    # 'preamble': '',
+    # Latex figure (float) alignment
+    # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+#  author, documentclass [howto, manual, or own class]).
+latex_documents = [
+    (
+        master_doc,
+        "Adafruit_blinkadisplayioLibrary.tex",
+        "Adafruit_blinkadisplayio Library Documentation",
+        author,
+        "manual",
+    ),
+]
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    (
+        master_doc,
+        "Adafruit_blinkadisplayiolibrary",
+        "Adafruit_blinka displayio Library Documentation",
+        [author],
+        1,
+    ),
+]
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+    (
+        master_doc,
+        "Adafruit_blinkadisplayioLibrary",
+        "Adafruit_blinka displayio Library Documentation",
+        author,
+        "Adafruit_blinkadisplayioLibrary",
+        "One line description of project.",
+        "Miscellaneous",
+    ),
+]
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644 (file)
index 0000000..26f853a
--- /dev/null
@@ -0,0 +1,40 @@
+.. include:: ../README.rst
+
+Table of Contents
+=================
+
+.. toctree::
+    :maxdepth: 4
+    :hidden:
+
+    self
+
+.. toctree::
+    :caption: API Reference
+    :maxdepth: 3
+
+    api
+
+.. toctree::
+    :caption: Tutorials
+
+.. toctree::
+    :caption: Related Products
+
+.. toctree::
+    :caption: Other Links
+
+    Download <https://github.com/adafruit/Adafruit_Blinka_CircuitPython_displayio/releases/latest>
+    CircuitPython Reference Documentation <https://circuitpython.readthedocs.io>
+    CircuitPython Support Forum <https://forums.adafruit.com/viewforum.php?f=60>
+    Discord Chat <https://adafru.it/discord>
+    Adafruit Learning System <https://learn.adafruit.com>
+    Adafruit Blog <https://blog.adafruit.com>
+    Adafruit Store <https://www.adafruit.com>
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/fontio.py b/fontio.py
new file mode 100644 (file)
index 0000000..cbb57d6
--- /dev/null
+++ b/fontio.py
@@ -0,0 +1,117 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+"""
+`fontio`
+================================================================================
+
+fontio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+from PIL import ImageFont
+from displayio import Bitmap
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
+
+class BuiltinFont:
+    """Simulate a font built into CircuitPython"""
+
+    def __init__(self):
+        self._font = ImageFont.load_default()
+        self._generate_bitmap(0x20, 0x7E)
+
+    def _generate_bitmap(self, start_range, end_range):
+        char_width, char_height = self.get_bounding_box()
+        self._bitmap = Bitmap(
+            char_width * (end_range - start_range + 1), char_height, 2
+        )
+        for character in range(start_range, end_range + 1):
+            ascii_char = chr(character)
+            ascii_mask = self._font.getmask(ascii_char, mode="1")
+            for y in range(char_height):
+                for x in range(char_width):
+                    color = ascii_mask.getpixel((x, y))
+                    character_position = character - start_range
+                    self._bitmap[character_position * char_width + x, y] = (
+                        1 if color else 0
+                    )
+
+    def get_bounding_box(self):
+        """Returns the maximum bounds of all glyphs in the font in
+        a tuple of two values: width, height.
+        """
+        return self._font.getsize("M")
+
+    def get_glyph(self, codepoint):
+        """Returns a `fontio.Glyph` for the given codepoint or None if no glyph is available."""
+        if 0x20 <= codepoint <= 0x7E:
+            glyph_index = codepoint - 0x20
+        else:
+            return None
+
+        bounding_box = self._font.getsize(chr(codepoint))
+        width, height = bounding_box
+        return Glyph(
+            bitmap=self._bitmap,
+            tile_index=glyph_index,
+            width=width,
+            height=height,
+            dx=0,
+            dy=0,
+            shift_x=width,
+            shift_y=0,
+        )
+
+    @property
+    def bitmap(self):
+        """Bitmap containing all font glyphs starting with ASCII and followed by unicode. Use
+        `get_glyph` in most cases. This is useful for use with `displayio.TileGrid` and
+        `terminalio.Terminal`.
+        """
+        return self._bitmap
+
+
+# pylint: disable=too-few-public-methods, invalid-name
+class Glyph:
+    """Storage of glyph info"""
+
+    def __init__(self, *, bitmap, tile_index, width, height, dx, dy, shift_x, shift_y):
+        self.bitmap = bitmap
+        self.width = width
+        self.height = height
+        self.dx = dx
+        self.dy = dy
+        self.shift_x = shift_x
+        self.shift_y = shift_y
+        self.tile_index = tile_index
+
+
+# pylint: enable=too-few-public-methods, invalid-name
diff --git a/requirements.txt b/requirements.txt
new file mode 100644 (file)
index 0000000..cc712ce
--- /dev/null
@@ -0,0 +1,4 @@
+Adafruit-Blinka
+pillow
+numpy
+recordclass
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..e2a138f
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,51 @@
+"""A setuptools based setup module.
+
+See:
+https://packaging.python.org/en/latest/distributing.html
+https://github.com/pypa/sampleproject
+"""
+
+from setuptools import setup, find_packages
+
+# To use a consistent encoding
+from codecs import open
+from os import path
+
+here = path.abspath(path.dirname(__file__))
+
+# Get the long description from the README file
+with open(path.join(here, "README.rst"), encoding="utf-8") as f:
+    long_description = f.read()
+
+setup(
+    name="adafruit-blinka-displayio",
+    use_scm_version=True,
+    setup_requires=["setuptools_scm"],
+    description="displayio for Blinka",
+    long_description=long_description,
+    long_description_content_type="text/x-rst",
+    # The project's main homepage.
+    url="https://github.com/adafruit/Adafruit_Blinka_Displayio",
+    # Author details
+    author="Adafruit Industries",
+    author_email="circuitpython@adafruit.com",
+    install_requires=["Adafruit-Blinka", "pillow", "numpy", "recordclass",],
+    # Choose your license
+    license="MIT",
+    # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
+    classifiers=[
+        "Development Status :: 3 - Alpha",
+        "Intended Audience :: Developers",
+        "Topic :: Software Development :: Libraries",
+        "Topic :: System :: Hardware",
+        "License :: OSI Approved :: MIT License",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.4",
+        "Programming Language :: Python :: 3.5",
+    ],
+    # What does your project relate to?
+    keywords="adafruit blinka circuitpython micropython displayio lcd tft display pitft",
+    # You can just specify the packages manually here if your project is
+    # simple. Or you can use find_packages().
+    py_modules=["displayio", "fontio", "terminalio"],
+)
diff --git a/terminalio.py b/terminalio.py
new file mode 100644 (file)
index 0000000..49ac368
--- /dev/null
@@ -0,0 +1,48 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+`terminalio`
+================================================================================
+
+terminalio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+import sys  # pylint: disable=unused-import
+import fontio
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
+FONT = fontio.BuiltinFont()
+
+# TODO: Tap into stdout to get the REPL
+# sys.stdout = open('out.dat', 'w')
+# sys.stdout.close()