diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..fe0b595
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,15 @@
+[run]
+branch = True
+omit =
+  pyglet/extlibs/*
+
+[report]
+exclude_lines =
+  pragma: no cover
+  if _debug:
+  @abstractmethod
+  pass
+
+[html]
+directory = coverage_report
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ac1ae7c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,51 @@
+# Python
+__pycache__
+*.pyc
+*.pyo
+
+# Virtualenvs (from project root)
+/env
+/.env
+/.venv
+/.venv2
+/.venv3
+
+# Build / Docs
+_build
+pyglet.*.log
+*.orig
+build
+dist
+pyglet.egg-info/
+MANIFEST
+.coverage
+coverage_report/
+
+# Version control
+.hgrc
+
+# IDEs / Editors
+.idea
+.vscode
+*~
+*.bak
+*.cache
+
+# Platforms
+.DS_Store
+*.swp
+
+# Internal
+tests/regression/images/*.png
+tests/interactive/screenshots/session/*.png
+tests/interactive/screenshots/committed/*.png
+doc/api
+doc/internal/build.rst
+website/dist
+tools/*tab.py
+exprtab.py
+parser.out
+parsetab.py
+pptab.py
+strict_exprtab.py
+prof/
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 0000000..9c5f69e
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,15 @@
+# Required
+version: 2
+
+# Build documentation in the docs/ directory with Sphinx
+sphinx:
+  configuration: doc/conf.py
+
+formats:
+  - epub
+  - htmlzip
+
+python:
+  version: 3.7
+  install:
+    - requirements: doc/requirements.txt
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..16e64f5
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,31 @@
+dist: xenial
+services: xvfb
+
+addons:
+  apt:
+    update: true
+    packages:
+      - freeglut3-dev
+
+language: python
+matrix:
+  include:
+    - name: "Python 3.7 on Linux"
+    - python:
+        - "3.7"
+        - "3.8"
+        - "pypy3"
+    - name: "Python 3.7 on MacOS"
+      os: osx
+      osx_image: xcode11
+      language: shell
+
+before_install: pip3 install --upgrade pip flake8 pytest lektor
+install: python3 setup.py install
+
+before_script: flake8 . --count --exclude=./.*,*/future/* --select=E9,F63,F7,F82 --show-source --statistics || true
+script: pytest --ignore=tests/base --ignore=tests/interactive --ignore=tests/integration/media --ignore=tests/integration/font --ignore=tests/integration/platform/test_linux_fontconfig.py
+
+deploy:
+  provider: script
+  script: lektor --project website deploy
diff --git a/DESIGN b/DESIGN
new file mode 100644
index 0000000..60edead
--- /dev/null
+++ b/DESIGN
@@ -0,0 +1,135 @@
+Principles
+----------
+
+* pyglet is an umbrella framework for games, multimedia and graphics
+  applications written in Python.
+* No required dependencies where possible.  Instead, core services provided
+  by Windows, OS X and X11 are used.  Some exceptions will have to apply (see
+  below)
+* It is not a game engine.  It's a set of modules that might be helpful,
+  and happen to work well together.
+* Not a single monolithic download.  Use .eggs to bundle related functionality
+  (core, audio, 2d, 3d, ...)
+
+
+The modules
+-----------
+
+Core modules (required by all others):
+
+pyglet.GL, pyglet.GLU
+  OpenGL, including all extensions and versions to 2.0.  This is a very
+  lightweight wrap, and requires knowledge of ctypes to use.  An application
+  developer writing an OpenGL application would want to use PyOpenGL or
+  OpenGL-ctypes instead, but pyglet itself only uses this (mixing and
+  matching is no problem).
+
+pyglet.shader
+  Shader management goes here. Might also have some shaders.
+
+pyglet.window
+  Interface for opening one or more windows with an OpenGL context, and
+  receiving and processing events on those windows.  GL contexts can
+  be separate, shared textures/lists or shared state between windows
+  (separate is default).  Include AGL, GLX, WGL and respective extensions.
+
+pyglet.clock
+  High-resolution timing, frames-per-second calculation (and display?)
+  and framerate limiting.
+
+pyglet.image
+  Load and save PNG.  Load DXT.  Load and save JPEG.
+  Images as both bitmaps and textures.
+
+  :Image:          raw image data with attributes width, height, bpp
+  :TextureOptions: as per blur.py
+  :Texture:        single image as texture with width, height, draw()
+  :TextureAtlas:   split a large texture into a grid of subtextures
+                   {row, col: Texture}, draw(row, col)
+  :PackedTexture:  pack many texture images as subtextures
+                   {name: Texture}
+  :RenderBuffer:   as per blur.py
+
+  Allowing texture etc. creation from PIL images should be possible.
+
+
+Optional modules, in approximate increasing pieness of sky:
+
+pyglet.font  
+  Rendering and layout of fonts, using Freetype, Windows and OS X for
+  rasterisation.  Includes the Bitstream family of fonts.  
+
+  Basic interface will have:
+
+  :Font:  a font file
+  :Glyph: describes a glyph from the font
+  :Text:  encapsulates a set of Glpyhs in a display list and
+          incorporates kerning in the glyph positioning
+
+  Rendering will be done to a texture. We should try to pack >1 rendered
+  glyph into a texture. Possibly pre-render the ASCII or latin-1 characters
+  when the font is loaded? Possibly just use PackedTexture?
+  
+pyglet.gui (requires pyglet.font)
+  Buttons, sliders, text entry, scrollable text, menus and lists.  Widgets
+  can be decorated with a pluggable look-and-feel (useful for quick mockups,
+  level-editors, graphics applications), or with customized images for
+  each widget (game interfaces).  Widgets can be laid out by pixel coordinates
+  (in an editor?), or with simple layout managers.  Transition effects
+  can be applied for buttons sliding on/off screen, fading in/out, rollovers,
+  crossfading, etc.  Command events etc are pushed back through the
+  pyglet.window event queue?  
+
+  Status elements like progress bars:
+
+    straight horizontal / vertical bars (active/inactive colour)
+    image-based where the image is "filled"  (active/inactive image)
+
+     - <ah>: These are the same thing, and should be controlled by look-n-feel
+             object.
+
+  Graphical elements like boxes to put other elements in which have
+  padding, border, margin like CSS box model.
+
+    - <ah>: Border should be abstracted and controlled by look-n-feel.
+            CSS model possibly too complicated and obfuscated?
+
+pyglet.draw
+  Draw ellipses, polygons, rectangles (using GLU?).
+
+pyglet.scene2d  (alternate name suggestions welcome)
+  2D sprites with collision detection, square and hexagon tile maps.  A
+  level editor.  Suitable for side-scrolling, top-down, isometric or
+  flat 3d rendered games. BTree.
+
+pyglet.scene3d  (alternate name suggestions welcome)
+  OBJ (and other formats?) model loading.  Models are readily modifiable
+  for vertex weighting, edge extraction (volume shadowing), binormal
+  calculations, etc.  Scene of objects, lights and camera.  Abstract mechanism
+  for frustum culling and collision detection. A scene editor. Octree,
+  possibly BSP.
+
+pyglet.euclid  (alternate name suggestions welcome)
+  2D and 3D vectors, matrices, quaternions and primitives such as sphere,
+  circle, line, ray, plane, etc.  Collision detection and simple resolution.
+
+pyglet.audio
+  Load, mix and play Wave and MP3 (minimum, more formats better) using
+  gstreamer, DirectAudio, Windows Media Player, Quicktime, CoreAudio, etc.
+  3D positional sound?
+
+pyglet.video
+  Play video (e.g., MPEG2) into a texture using gstreamer, Windows Media
+  Player, Quicktime, CoreVideo, etc.
+
+pyglet.joystick
+  Include force feedback.
+
+pyglet.network
+  Network events handled in a similar manner to window events. Abstracted
+  interface to TCP and UDP, similar to nanotubes from FibraNet?
+  http://code.google.com/p/fibranet/
+
+pyglet.ai
+  A*, state machine, thoughts?  (no pun intended)
+
diff --git a/LICENSE b/LICENSE
index ee381d3..c0693ca 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,6 @@
 Copyright (c) 2006-2008 Alex Holkner
-Copyright (c) 2008-2019 pyglet contributorsAll rights reserved.
+Copyright (c) 2008-2021 pyglet contributors
+All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are met:
diff --git a/PKG-INFO b/PKG-INFO
index f646cd4..e68585a 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: pyglet
-Version: 1.5.14
+Version: 1.5.24
 Summary: Cross-platform windowing and multimedia library
 Home-page: http://pyglet.readthedocs.org/en/latest/
 Author: Alex Holkner
@@ -10,150 +10,6 @@ Download-URL: http://pypi.python.org/pypi/pyglet
 Project-URL: Documentation, https://pyglet.readthedocs.io/en/latest
 Project-URL: Source, https://github.com/pyglet/pyglet
 Project-URL: Tracker, https://github.com/pyglet/pyglet/issues
-Description: [![pypi](https://badge.fury.io/py/pyglet.svg)](https://pypi.python.org/pypi/pyglet) [![rtd](https://readthedocs.org/projects/pyglet/badge/?version=latest)](https://pyglet.readthedocs.io)
-        
-        ![logo_large.png](https://bitbucket.org/repo/aejyXX/images/3385888514-logo_large.png)
-        
-        # pyglet
-        
-        *pyglet* is a cross-platform windowing and multimedia library for Python, intended for developing games
-        and other visually rich applications. It supports windowing, user interface event handling, Joysticks,
-        OpenGL graphics, loading images and videos, and playing sounds and music. *pyglet* works on Windows, OS X and Linux.
-        
-        * pyglet [documentation]
-        * pyglet [wiki]
-        * pyglet on [PyPI]
-        * pyglet [discord] server
-        * pyglet [mailing list]
-        * pyglet [issue tracker]
-        * pyglet [website]
-        
-        Pyglet has an active developer and user community.  If you find a bug or a problem with the documentation,
-        please [open an issue](https://github.com/pyglet/pyglet/issues).
-        Anyone is welcome to join our [discord] server where a lot of the development discussion is going on.
-        It's also a great place to ask for help.
-        
-        Some of the features of pyglet are:
-        
-        * **No external dependencies or installation requirements.** For most application and game requirements, *pyglet*
-          needs nothing else besides Python, simplifying distribution and installation. It's easy to package your project
-          with freezers such as PyInstaller. 
-        * **Take advantage of multiple windows and multi-monitor desktops.** *pyglet* allows you to use multiple
-          platform-native windows, and is fully aware of multi-monitor setups for use with fullscreen games.
-        * **Load images, sound, music and video in almost any format.** *pyglet* can optionally use FFmpeg to play back
-          audio formats such as MP3, OGG/Vorbis and WMA, and video formats such as MPEG2, H.264, H.265, WMV and Xvid.
-          Without FFmpeg, *pyglet* contains built-in support for standard formats such as wav, png, bmp, and others.
-        * **pyglet is written entirely in pure Python**, and makes use of the *ctypes* module to interface with system
-          libraries. You can modify the codebase or make a contribution without any second language compilation steps or
-          compiler setup. Despite being pure Python, *pyglet* has excellent performance thanks to advanced batching for
-          drawing thousands of objects.
-        * **pyglet is provided under the BSD open-source license**, allowing you to use it for both commercial and other
-          open-source projects with very little restriction.
-        
-        ## Requirements
-        
-        Pyglet runs under Python 3.5+. Being written in pure Python, it also works on other Python
-        interpreters such as PyPy. Supported platforms are:
-        
-        * Windows 7 or later
-        * Mac OS X 10.3 or later
-        * Linux, with the following libraries (most recent distributions will have
-          these in a default installation):
-          * OpenGL and GLX
-          * GDK 2.0+ or Pillow (required for loading images other than PNG and BMP)
-          * OpenAL or Pulseaudio (required for playing audio)
-        
-        **Please note that pyglet v1.5 will likely be the last version to support
-        legacy OpenGL**. Future releases of pyglet will be targeting OpenGL 3.3+.
-        Previous releases will remain available for download.
-        
-        Starting with version 1.4, to play compressed audio and video files,
-        you will also need [FFmpeg](https://ffmpeg.org/).
-        
-        ## Installation
-        
-        pyglet is installable from PyPI:
-        
-            pip install --upgrade --user pyglet
-        
-        ## Installation from source
-        
-        If you're reading this `README` from a source distribution, you can install pyglet with:
-        
-            python setup.py install --user
-        
-        You can also install the latest development version direct from Github using:
-        
-            pip install --upgrade --user https://github.com/pyglet/pyglet/archive/master.zip
-        
-        For local development install pyglet in editable mode:
-        
-        ```bash
-        # with pip
-        pip install -e .
-        # with setup.py
-        python setup.py develop
-        ```
-        
-        There are no compilation steps during the installation; if you prefer,
-        you can simply add this directory to your `PYTHONPATH` and use pyglet without
-        installing it. You can also copy pyglet directly into your project folder.
-        
-        ## Contributing
-        
-        **A good way to start contributing to a component of pyglet is by its documentation**. When studying the code you
-        are going to work with, also read the associated docs. If you don't understand the code with the help of the docs,
-        it is a sign that the docs should be improved.
-        
-        If you want to contribute to pyglet, we suggest the following:
-        
-        * Fork the [official repository](https://github.com/pyglet/pyglet/fork).
-        * Apply your changes to your fork.
-        * Submit a [pull request](https://github.com/pyglet/pyglet/pulls) describing the changes you have made.
-        * Alternatively you can create a patch and submit it to the issue tracker.
-        
-        When making a pull request, check that you have addressed its respective documentation, both within the code docstrings
-        and the programming guide (if applicable). It is very important to all of us that the documentation matches the latest
-        code and vice-versa.
-        
-        Consequently, an error in the documentation, either because it is hard to understand or because it doesn't match the
-        code, is a bug that deserves to be reported on a ticket.
-        
-        ## Building Docs
-        
-            pip install -r doc/requirements.txt
-            python setup.py build_sphinx
-        
-        Please check [the README.md file in the doc directory](doc/README.md) for more details.
-        
-        ## Testing
-        
-        pyglet makes use of pytest for its test suite.
-        
-        ```bash
-        pip install -r tests/requirements.txt --user
-        # Only run unittests
-        pytest tests/unit
-        ```
-        
-        Please check the [testing section in the development guide](https://pyglet.readthedocs.io/en/latest/internal/testing.html)
-        for more information about running and writing tests.
-        
-        ## Contact
-        
-        pyglet is developed by many individual volunteers, and there is no central point of contact. If you have a question
-        about developing with pyglet, or you wish to contribute, please join the [mailing list] or the [discord] server.
-        
-        For legal issues, please contact [Alex Holkner](mailto:Alex.Holkner@gmail.com).
-        
-        [discord]: https://discord.gg/QXyegWe
-        [mailing list]: http://groups.google.com/group/pyglet-users
-        [documentation]: https://pyglet.readthedocs.io
-        [wiki]:  https://github.com/pyglet/pyglet/wiki
-        [pypi]:  https://pypi.org/project/pyglet/
-        [website]: http://pyglet.org/
-        [issue tracker]: https://github.com/pyglet/pyglet/issues
-        
 Platform: UNKNOWN
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Environment :: MacOS X
@@ -165,10 +21,158 @@ Classifier: Operating System :: MacOS :: MacOS X
 Classifier: Operating System :: Microsoft :: Windows
 Classifier: Operating System :: POSIX :: Linux
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.5
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
 Classifier: Topic :: Games/Entertainment
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
 Description-Content-Type: text/markdown
+License-File: LICENSE
+License-File: NOTICE
+
+[![pypi](https://badge.fury.io/py/pyglet.svg)](https://pypi.python.org/pypi/pyglet) [![rtd](https://readthedocs.org/projects/pyglet/badge/?version=latest)](https://pyglet.readthedocs.io)
+
+![logo_large.png](https://bitbucket.org/repo/aejyXX/images/3385888514-logo_large.png)
+
+# pyglet
+
+*pyglet* is a cross-platform windowing and multimedia library for Python, intended for developing games
+and other visually rich applications. It supports windowing, user interface event handling, Joysticks,
+OpenGL graphics, loading images and videos, and playing sounds and music. *pyglet* works on Windows, OS X and Linux.
+
+* pyglet [documentation]
+* pyglet [wiki]
+* pyglet on [PyPI]
+* pyglet [discord] server
+* pyglet [mailing list]
+* pyglet [issue tracker]
+* pyglet [website]
+
+Pyglet has an active developer and user community.  If you find a bug or a problem with the documentation,
+please [open an issue](https://github.com/pyglet/pyglet/issues).
+Anyone is welcome to join our [discord] server where a lot of the development discussion is going on.
+It's also a great place to ask for help.
+
+Some features of pyglet are:
+
+* **No external dependencies or installation requirements.** For most application and game requirements, *pyglet*
+  needs nothing else besides Python, simplifying distribution and installation. It's easy to package your project
+  with freezers such as PyInstaller. 
+* **Take advantage of multiple windows and multi-monitor desktops.** *pyglet* allows you to use multiple
+  platform-native windows, and is fully aware of multi-monitor setups for use with fullscreen games.
+* **Load images, sound, music and video in almost any format.** *pyglet* can optionally use FFmpeg to play back
+  audio formats such as MP3, OGG/Vorbis and WMA, and video formats such as MPEG2, H.264, H.265, WMV and Xvid.
+  Without FFmpeg, *pyglet* contains built-in support for standard formats such as wav, png, bmp, and others.
+* **pyglet is written entirely in pure Python**, and makes use of the *ctypes* module to interface with system
+  libraries. You can modify the codebase or make a contribution without any second language compilation steps or
+  compiler setup. Despite being pure Python, *pyglet* has excellent performance thanks to advanced batching for
+  drawing thousands of objects.
+* **pyglet is provided under the BSD open-source license**, allowing you to use it for both commercial and other
+  open-source projects with very little restriction.
+
+## Requirements
+
+pyglet runs under Python 3.6+. Being written in pure Python, it also works on other Python
+interpreters such as PyPy. Supported platforms are:
+
+* Windows 7 or later
+* Mac OS X 10.3 or later
+* Linux, with the following libraries (most recent distributions will have
+  these in a default installation):
+  * OpenGL and GLX
+  * GDK 2.0+ or Pillow (required for loading images other than PNG and BMP)
+  * OpenAL or Pulseaudio (required for playing audio)
+
+**Please note that pyglet v1.5 will likely be the last version to support
+legacy OpenGL**. Future releases of pyglet will be targeting OpenGL 3.3+.
+Previous releases will remain available for download.
+
+Starting with version 1.4, to play compressed audio and video files,
+you will also need [FFmpeg](https://ffmpeg.org/).
+
+## Installation
+
+pyglet is installable from PyPI:
+
+    pip install --upgrade --user pyglet
+
+## Installation from source
+
+If you're reading this `README` from a source distribution, you can install pyglet with:
+
+    python setup.py install --user
+
+You can also install the latest development version direct from Github using:
+
+    pip install --upgrade --user https://github.com/pyglet/pyglet/archive/master.zip
+
+For local development install pyglet in editable mode:
+
+```bash
+# with pip
+pip install -e .
+# with setup.py
+python setup.py develop
+```
+
+There are no compilation steps during the installation; if you prefer,
+you can simply add this directory to your `PYTHONPATH` and use pyglet without
+installing it. You can also copy pyglet directly into your project folder.
+
+## Contributing
+
+**A good way to start contributing to a component of pyglet is by its documentation**. When studying the code you
+are going to work with, also read the associated docs. If you don't understand the code with the help of the docs,
+it is a sign that the docs should be improved.
+
+If you want to contribute to pyglet, we suggest the following:
+
+* Fork the [official repository](https://github.com/pyglet/pyglet/fork).
+* Apply your changes to your fork.
+* Submit a [pull request](https://github.com/pyglet/pyglet/pulls) describing the changes you have made.
+* Alternatively you can create a patch and submit it to the issue tracker.
+
+When making a pull request, check that you have addressed its respective documentation, both within the code docstrings
+and the programming guide (if applicable). It is very important to all of us that the documentation matches the latest
+code and vice-versa.
+
+Consequently, an error in the documentation, either because it is hard to understand or because it doesn't match the
+code, is a bug that deserves to be reported on a ticket.
+
+## Building Docs
+
+    pip install -r doc/requirements.txt
+    python setup.py build_sphinx
+
+Please check [the README.md file in the doc directory](doc/README.md) for more details.
+
+## Testing
+
+pyglet makes use of pytest for its test suite.
+
+```bash
+pip install -r tests/requirements.txt --user
+# Only run unittests
+pytest tests/unit
+```
+
+Please check the [testing section in the development guide](https://pyglet.readthedocs.io/en/latest/internal/testing.html)
+for more information about running and writing tests.
+
+## Contact
+
+pyglet is developed by many individual volunteers, and there is no central point of contact. If you have a question
+about developing with pyglet, or you wish to contribute, please join the [mailing list] or the [discord] server.
+
+For legal issues, please contact [Alex Holkner](mailto:Alex.Holkner@gmail.com).
+
+[discord]: https://discord.gg/QXyegWe
+[mailing list]: http://groups.google.com/group/pyglet-users
+[documentation]: https://pyglet.readthedocs.io
+[wiki]:  https://github.com/pyglet/pyglet/wiki
+[pypi]:  https://pypi.org/project/pyglet/
+[website]: http://pyglet.org/
+[issue tracker]: https://github.com/pyglet/pyglet/issues
+
+
diff --git a/README.md b/README.md
index 7810a41..92cdaef 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ please [open an issue](https://github.com/pyglet/pyglet/issues).
 Anyone is welcome to join our [discord] server where a lot of the development discussion is going on.
 It's also a great place to ask for help.
 
-Some of the features of pyglet are:
+Some features of pyglet are:
 
 * **No external dependencies or installation requirements.** For most application and game requirements, *pyglet*
   needs nothing else besides Python, simplifying distribution and installation. It's easy to package your project
@@ -40,7 +40,7 @@ Some of the features of pyglet are:
 
 ## Requirements
 
-Pyglet runs under Python 3.5+. Being written in pure Python, it also works on other Python
+pyglet runs under Python 3.6+. Being written in pure Python, it also works on other Python
 interpreters such as PyPy. Supported platforms are:
 
 * Windows 7 or later
diff --git a/RELEASE_NOTES b/RELEASE_NOTES
index d6525b1..d989579 100644
--- a/RELEASE_NOTES
+++ b/RELEASE_NOTES
@@ -1,3 +1,160 @@
+pyglet 1.5.24
+
+Features
+--------
+- Add initial support for FFmpeg 5.0
+- Windows - The GDI font renderer now supports Unicode font names.
+
+Bugfixes
+--------
+- Windows - Fix `on_resize` event not dispatched when `set_size` is called.
+- Documentation updates, fixed links, and corrections.
+- Windows - Fix crash when font characters are more than one codepoint in length.
+
+
+pyglet 1.5.23
+
+Features
+--------
+- Windows - Add `win32_disable_shaping` option. This can improve font performance when shaping isn't required.
+- Backport the latest pyglet.math module changes from the development branch.
+
+Bugfixes
+--------
+- Linux - Fix crash when Gstreamer Gst bindings are not installed.
+- Fix math.Vec4.clamp method.
+
+
+pyglet 1.5.22
+
+Features
+--------
+- If the PyOgg module is installed, it can be used for loading various Vorbis audio formats.
+- Add a new CameraGroup example, to show implementing a Camera with pyglet's Groups.
+- Add `angle` and `start_angle` property/setter to `shapes.Sector`.
+- Windows - new WMF based encoder for faster saving of common image formats.
+
+Bugfixes
+--------
+- Fix indexing error when setting text.Label.opacity (#481)
+- Windows - Fix shift modifier + exclusive mouse mode (#472)
+- Linux - Prevent non-Tablet devices from being detected as Tablets (#491)
+- Windows - Prevent distortion with multiple XAudio2 audio sources (#515)
+- Fix frame dropping bug with FFMpeg decoder.
+- Windows - Fix Video alpha channel for WMF decoder.
+- Varios documentation and docstring fixes. Thanks everyone!
+
+
+pyglet 1.5.21
+
+Features
+--------
+- A new MovableFrame that allows repositioning Widgets when a specified key modifier is held.
+- Text Layouts now have `opacity` and `visible` attributes, similar to Sprites.
+- Add new shapes.Ellipse class.
+
+Bugfixes
+--------
+- Xlib - don't enable certain Window options if transparency is not enabled. (#454)
+- Windows - Fix issue with some fonts where glyphs overhanging their advance would be cut off.
+
+
+pyglet 1.5.20
+
+Features
+--------
+- Experimental support for transparent and overlay windows on Linux and Windows.
+- Shapes - Allow rotation and changing of border color for the BorderedRectangle.
+
+Bugfixes
+--------
+- Xlib - Fix the mouse Y position being off by 1-pixel.
+- Windows - Fix gapless audio playback on the XAudio2 backend.
+
+
+pyglet 1.5.19
+
+Features
+--------
+- Add new 'Sector' class to shapes module, for creating sectors of a circle.
+
+Bugfixes
+--------
+- Ensure that the FFmpegDecoder returns the requested number of bytes.
+- When subclassing EventHandlers, ensure that possible TyperErrors give correct feedback.
+- Fix missing name attribute on FreeTypeMemoryFace object when adding fonts.
+
+Maintenance
+-----------
+- Re-introduce background threads for refilling Player buffers.
+- Allow directly setting Widget values.
+- Add docstrings for gui.widgets.
+- Refresh of experimental/win32priority.py.
+
+
+pyglet 1.5.18
+Bugfix release
+
+Bugfixes
+--------
+- If XAudio2 device creation fails, catch exception so that the next driver can be tried.
+- Fix dangling file pointers in GStreamer decoder.
+- Expose font.name attribute to show the font family name.
+- Un-associate queued Source from a deleted Player instance (#256)
+- Fix circular import when trying to create a Windows in 'headless' mode.
+- Un-associate StreamingSources from deleted Player instances (#256)
+- Update pypng lib to avoid deprecated functions (#405)
+
+
+pyglet 1.5.17
+Bugfix, feature and maintenance release
+
+Bugfixes
+--------
+- FFmpeg decoder add FF_INPUT_BUFFER_PADDING_SIZE to buffers.
+- Add missing DI8DEVTYPE_SUPPLEMENTAL joystick device type.
+- Fix bool clamping causing crash with DirectWrite text decoder.
+
+Maintenance
+-----------
+- Change IncrementalTextLayout to use glScissor instead of glClipPlane.
+- Raise warning on Window creation if the GPU drivers do not support OpenGL 2.0
+
+Features
+--------
+- Add a new `shapes.Star` shape.
+
+
+pyglet 1.5.16
+Bugfix and feature release
+
+Bugfixes
+--------
+- 3d model obj decoder supports multiple material types
+- Fix GStreamerSources not being garbage collected #283
+- Fix ScrollableTextLayout not respecting anchors/alignment.
+
+Features
+--------
+- New DirectWrite based font loader. Enable with `pyglet.options["advanced_font_features"] = True`
+- Add `position` property to Text Layouts, to mimic other classes.
+
+
+pyglet 1.5.15
+Bugfix and maintenance release
+
+Bugfixes
+--------
+- shapes.Circle segment calculation will always use a minimum of 14 segments.
+- shapes.Arc is now made from line segments, and by default has unconnected ends.
+- Windows - Use the internal keystate to determine the mod shift rather than relying on GetKeyState
+  which relies on another event that may be called after WM_INPUT.
+
+Maintenance
+-----------
+- Various Headless backend and EGL work.
+
+
 pyglet 1.5.14
 Bugfix and feature release
 
diff --git a/contrib/aseprite_codec/README b/contrib/aseprite_codec/README
new file mode 100644
index 0000000..d73dc8e
--- /dev/null
+++ b/contrib/aseprite_codec/README
@@ -0,0 +1,14 @@
+This is an example of a custom image codec.
+Custom codecs can be created and added at runtime
+in your application, allowing you to add support
+for loading specialized image and animation formats.
+
+This codec adds support for the Aseprite binary
+format, which is a popular software for creating
+animated sprites.
+
+https://www.aseprite.org/
+https://github.com/aseprite/aseprite/blob/master/docs/ase-file-specs.md
+
+This codec is fairly complete, with the limitation
+of only supporting "normal" blending mode for layers.
\ No newline at end of file
diff --git a/contrib/aseprite_codec/asedemo.py b/contrib/aseprite_codec/asedemo.py
new file mode 100644
index 0000000..06b9b28
--- /dev/null
+++ b/contrib/aseprite_codec/asedemo.py
@@ -0,0 +1,30 @@
+import pyglet
+from pyglet.gl import *
+
+# Import the new codec
+import aseprite
+
+
+window = pyglet.window.Window()
+
+# Add image decoders from the loaded codec module.
+# This adds support to the image and resource modules.
+pyglet.image.codecs.add_decoders(aseprite)
+
+# You can now load a animation of *.ase type:
+image = pyglet.image.load_animation("running.ase")
+sprite = pyglet.sprite.Sprite(img=image, x=50, y=50)
+sprite.scale = 8
+
+
+@window.event
+def on_draw():
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
+    window.clear()
+    sprite.draw()
+
+
+if __name__ == "__main__":
+    pyglet.app.run()
+
diff --git a/contrib/aseprite_codec/aseprite.py b/contrib/aseprite_codec/aseprite.py
new file mode 100644
index 0000000..d19a876
--- /dev/null
+++ b/contrib/aseprite_codec/aseprite.py
@@ -0,0 +1,389 @@
+# ----------------------------------------------------------------------------
+# pyglet
+# Copyright (c) 2006-2008 Alex Holkner
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+#  * Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+#  * Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in
+#    the documentation and/or other materials provided with the
+#    distribution.
+#  * Neither the name of pyglet nor the names of its
+#    contributors may be used to endorse or promote products
+#    derived from this software without specific prior written
+#    permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# ----------------------------------------------------------------------------
+
+"""Decoder for Aseprite animation files in .ase or .aseprite format.
+"""
+
+import io
+import zlib
+import struct
+
+from pyglet.image import ImageData, Animation, AnimationFrame
+from pyglet.image.codecs import ImageDecoder, ImageDecodeException
+
+
+#   Documentation for the Aseprite format can be found here:
+#   https://raw.githubusercontent.com/aseprite/aseprite/master/docs/ase-file-specs.md
+
+
+BYTE = "B"
+WORD = "H"
+SHORT = "h"
+DWORD = "I"
+
+BLEND_MODES = {0: 'Normal',
+               1: 'Multiply',
+               2: 'Screen',
+               3: 'Overlay',
+               4: 'Darken',
+               5: 'Lighten',
+               6: 'Color Dodge',
+               7: 'Color Burn',
+               8: 'Hard Light',
+               9: 'Soft Light',
+               10: 'Difference',
+               11: 'Exclusion',
+               12: 'Hue',
+               13: 'Saturation',
+               14: 'Color',
+               15: 'Luminosity'}
+
+PALETTE_DICT = {}
+PALETTE_INDEX = 0
+
+
+def _unpack(fmt, file):
+    """Unpack little endian bytes fram a file-like object. """
+    size = struct.calcsize(fmt)
+    data = file.read(size)
+    if len(data) < size:
+        raise ImageDecodeException('Unexpected EOF')
+    return struct.unpack("<" + fmt, data)[0]
+
+
+def _chunked_iter(seq, size):
+    return (seq[pos:pos + size] for pos in range(0, len(seq), size))
+
+
+#########################################
+#   Class for Aseprite compliant header
+#########################################
+
+class AsepriteHeader:
+    def __init__(self, file):
+        self.file_size = _unpack(DWORD, file)
+        self.magic_number = hex(_unpack(WORD, file))
+        self.num_frames = _unpack(WORD, file)
+        self.width = _unpack(WORD, file)
+        self.height = _unpack(WORD, file)
+        self.color_depth = _unpack(WORD, file)
+        self.flags = _unpack(DWORD, file)
+        self.speed = _unpack(WORD, file)
+        self._zero = _unpack(DWORD, file)
+        self._zero = _unpack(DWORD, file)
+        self.palette_index = _unpack(BYTE, file)
+        self._ignore = _unpack(BYTE * 3, file)
+        self.number_of_colors = _unpack(WORD, file)
+        self._zero = _unpack(BYTE * 94, file)
+
+
+#########################################
+#   Class for Aseprite animation frames
+#########################################
+
+class Frame(object):
+    def __init__(self, num_chunks, duration, header, data):
+        self.num_chunks = num_chunks
+        self.duration = duration
+        self.color_depth = header.color_depth
+        self.width = header.width
+        self.height = header.height
+        self._data = data
+        self.chunks = self._parse_chunks()
+        self.cels = [c for c in self.chunks if type(c) == CelChunk]
+        self.layers = [c for c in self.chunks if type(c) == LayerChunk]
+
+    def _parse_chunks(self):
+        fileobj = io.BytesIO(self._data)
+        chunks = []
+        for chunk in range(self.num_chunks):
+            chunk_size = _unpack(DWORD, fileobj)
+            chunk_type = format(_unpack(WORD, fileobj), "#06x")
+            header_size = struct.calcsize(DWORD + WORD)
+            chunk_data = fileobj.read(chunk_size - header_size)
+            if chunk_type in ("0x0004", "0x0011", "0x2016"):
+                chunks.append(DeprecatedChunk(chunk_size, chunk_type, chunk_data))
+            elif chunk_type == "0x2004":
+                chunks.append(LayerChunk(chunk_size, chunk_type, chunk_data))
+            elif chunk_type == "0x2005":
+                chunks.append(CelChunk(chunk_size, chunk_type, chunk_data))
+            elif chunk_type == "0x2017":
+                chunks.append(PathChunk(chunk_size, chunk_type, chunk_data))
+            elif chunk_type == "0x2018":
+                chunks.append(FrameTagsChunk(chunk_size, chunk_type, chunk_data))
+            elif chunk_type == "0x2019":
+                palette_chunk = PaletteChunk(chunk_size, chunk_type, chunk_data)
+                chunks.append(palette_chunk)
+                global PALETTE_DICT
+                PALETTE_DICT = palette_chunk.palette_dict.copy()
+            elif chunk_type == "0x2020":
+                chunks.append(UserDataChunk(chunk_size, chunk_type, chunk_data))
+        return chunks
+
+    def _pad_pixels(self, cel):
+        """For cels that dont fill the entire frame, pad with zeros."""
+        fileobj = io.BytesIO(cel.pixel_data)
+
+        padding = b'\x00\x00\x00\x00'
+        top_pad = padding * (self.width * cel.y_pos)
+        left_pad = padding * cel.x_pos
+        right_pad = padding * (self.width - cel.x_pos - cel.width)
+        bottom_pad = padding * (self.width * (self.height - cel.height - cel.y_pos))
+        line_size = cel.width * 4   # (RGBA)
+
+        pixel_array = top_pad
+        for i in range(cel.height):
+            pixel_array += (left_pad + fileobj.read(line_size) + right_pad)
+        pixel_array += bottom_pad
+
+        return pixel_array
+
+    @staticmethod
+    def _blend_pixels(bottom, top, mode):
+        # Iterate over the arrays in chunks of 4 (RGBA):
+        bottom_iter = _chunked_iter(bottom, 4)
+        top_iter = _chunked_iter(top, 4)
+
+        if mode == 'Normal':
+            final_array = []
+            # If RGBA values are > 0, use the top pixel.
+            for bottom_pixel, top_pixel in zip(bottom_iter, top_iter):
+                if any(top_pixel) > 0:      # Any of R, G, B, A > 0
+                    final_array.extend(top_pixel)
+                else:
+                    final_array.extend(bottom_pixel)
+            return bytes(final_array)
+
+        # TODO: implement additional blend modes
+        else:
+            raise ImageDecodeException("Unsupported blend mode: '{}'".format(mode))
+
+    def _convert_to_rgba(self, cel):
+        if self.color_depth == 8:
+            global PALETTE_INDEX
+            pixel_array = []
+            for pixel in cel.pixel_data:
+                if pixel == PALETTE_INDEX:
+                    pixel_array.extend([0, 0, 0, 0])
+                else:
+                    pixel_array.extend(PALETTE_DICT[pixel])
+            cel.pixel_data = bytes(pixel_array)
+            return cel
+
+        elif self.color_depth == 16:
+            greyscale_iter = _chunked_iter(cel.pixel_data, 2)
+            pixel_array = []
+            for pixel in greyscale_iter:
+                rgba = (pixel[0] * 3) + pixel[1]
+                pixel_array.append(rgba)
+            cel.pixel_data = bytes(pixel_array)
+            return cel
+
+        else:
+            return cel
+
+    def get_pixel_array(self, layers):
+        # Start off with an empty RGBA base:
+        pixel_array = bytes(4 * self.width * self.height)
+
+        # Blend each layer's cel data one-by-one:
+        for cel in self.cels:
+            cel = self._convert_to_rgba(cel)
+            padded_pixels = self._pad_pixels(cel)
+            blend_mode = BLEND_MODES[layers[cel.layer_index].blend_mode]
+            pixel_array = self._blend_pixels(pixel_array, padded_pixels, blend_mode)
+
+        return pixel_array
+
+
+#########################################
+#   Aseprite Chunk type definitions
+#########################################
+
+class Chunk:
+    def __init__(self, size, chunk_type):
+        self.size = size
+        self.chunk_type = chunk_type
+
+
+class LayerChunk(Chunk):
+    def __init__(self, size, chunk_type, data):
+        super().__init__(size, chunk_type)
+        fileobj = io.BytesIO(data)
+        self.flags = _unpack(WORD, fileobj)
+        self.layer_type = _unpack(WORD, fileobj)
+        self.child_level = _unpack(WORD, fileobj)
+        _ignored_width = _unpack(WORD, fileobj)
+        _ignored_height = _unpack(WORD, fileobj)
+        self.blend_mode = _unpack(WORD, fileobj)
+        self.opacity = _unpack(BYTE, fileobj)
+        _zero_unused = _unpack(BYTE * 3, fileobj)
+        name_length = _unpack(WORD, fileobj)
+        self.name = fileobj.read(name_length)
+        if hasattr(self.name, "decode"):
+            self.name = self.name.decode('utf8')
+
+
+class CelChunk(Chunk):
+    def __init__(self, size, chunk_type, data):
+        super().__init__(size, chunk_type)
+        fileobj = io.BytesIO(data)
+        self.layer_index = _unpack(WORD, fileobj)
+        self.x_pos = _unpack(SHORT, fileobj)
+        self.y_pos = _unpack(SHORT, fileobj)
+        self.opacity_level = _unpack(BYTE, fileobj)
+        self.cel_type = _unpack(WORD, fileobj)
+        _zero_unused = _unpack(BYTE * 7, fileobj)
+        if self.cel_type == 0:
+            self.width = _unpack(WORD, fileobj)
+            self.height = _unpack(WORD, fileobj)
+            self.pixel_data = fileobj.read()
+        elif self.cel_type == 1:
+            self.frame_position = _unpack(WORD, fileobj)
+        elif self.cel_type == 2:
+            self.width = _unpack(WORD, fileobj)
+            self.height = _unpack(WORD, fileobj)
+            self.pixel_data = zlib.decompress(fileobj.read())
+
+
+class PathChunk(Chunk):
+    def __init__(self, size, chunk_type, data):
+        super().__init__(size, chunk_type)
+
+
+class FrameTagsChunk(Chunk):
+    def __init__(self, size, chunk_type, data):
+        super().__init__(size, chunk_type)
+        # TODO: unpack this data.
+
+
+class PaletteChunk(Chunk):
+    def __init__(self, size, chunk_type, data):
+        super().__init__(size, chunk_type)
+        fileobj = io.BytesIO(data)
+        self.palette_size = _unpack(DWORD, fileobj)
+        self.first_color_index = _unpack(DWORD, fileobj)
+        self.last_color_index = _unpack(DWORD, fileobj)
+        _zero = _unpack(BYTE * 8, fileobj)
+        self.palette_dict = {}
+        if _unpack(WORD, fileobj) == 1:              # color has name
+            size = 7
+        else:
+            size = 6
+        for index in range(self.first_color_index, self.last_color_index+1):
+            rgba_data = fileobj.read(size)
+            # Ignore the palette names, as they aren't needed:
+            r, g, b, a = struct.unpack('<BBBB', rgba_data[:4])
+            self.palette_dict[index] = r, g, b, a
+
+
+class UserDataChunk(Chunk):
+    def __init__(self, size, chunk_type, data):
+        super().__init__(size, chunk_type)
+        # TODO: unpack this data.
+
+
+class DeprecatedChunk(Chunk):
+    def __init__(self, size, chunk_type, data):
+        super().__init__(size, chunk_type)
+
+
+#########################################
+#   Image Decoder class definition
+#########################################
+
+class AsepriteImageDecoder(ImageDecoder):
+    def get_file_extensions(self):
+        return ['.ase', '.aseprite']
+
+    def get_animation_file_extensions(self):
+        return ['.ase', '.aseprite']
+
+    def decode(self, file, filename):
+        header, frames, layers, pitch = self._parse_file(file, filename)
+        pixel_data = frames[0].get_pixel_array(layers=layers)
+        return ImageData(header.width, header.height, 'RGBA', pixel_data, -pitch)
+
+    def decode_animation(self, file, filename):
+        header, frames, layers, pitch = self._parse_file(file, filename)
+        animation_frames = []
+        for frame in frames:
+            pixel_data = frame.get_pixel_array(layers=layers)
+            image = ImageData(header.width, header.height, 'RGBA', pixel_data, -pitch)
+            animation_frames.append(AnimationFrame(image, frame.duration/1000.0))
+        return Animation(animation_frames)
+
+    @staticmethod
+    def _parse_file(file, filename):
+        if not file:
+            file = open(filename, 'rb')
+
+        header = AsepriteHeader(file)
+        if header.magic_number != '0xa5e0':
+            raise ImageDecodeException("Does not appear to be a valid ASEprite file.")
+
+        if header.color_depth not in (8, 16, 32):
+            raise ImageDecodeException("Invalid color depth.")
+
+        global PALETTE_INDEX
+        PALETTE_INDEX = header.palette_index
+
+        frames = []
+        for _ in range(header.num_frames):
+            frame_size = _unpack(DWORD, file)
+            magic_number = hex(_unpack(WORD, file))
+            if magic_number != '0xf1fa':
+                raise ImageDecodeException("Malformed frame. File may be corrupted.")
+            num_chunks = _unpack(WORD, file)
+            duration = _unpack(WORD, file)
+            _zero = _unpack(BYTE * 6, file)
+            header_size = struct.calcsize(DWORD + WORD * 3 + BYTE * 6)
+            data = file.read(frame_size - header_size)
+            frames.append(Frame(num_chunks, duration, header, data))
+
+        # Layers chunk is in the first frame:
+        layers = frames[0].layers
+        pitch = len('RGBA') * header.width
+
+        file.close()
+
+        return header, frames, layers, pitch
+
+
+def get_decoders():
+    return [AsepriteImageDecoder()]
+
+
+def get_encoders():
+    return []
diff --git a/contrib/aseprite_codec/running.ase b/contrib/aseprite_codec/running.ase
new file mode 100644
index 0000000..f13bed2
Binary files /dev/null and b/contrib/aseprite_codec/running.ase differ
diff --git a/contrib/logo3d/logo3d.jpg b/contrib/logo3d/logo3d.jpg
new file mode 100644
index 0000000..49e8185
Binary files /dev/null and b/contrib/logo3d/logo3d.jpg differ
diff --git a/contrib/logo3d/logo3d.obj.zip b/contrib/logo3d/logo3d.obj.zip
new file mode 100644
index 0000000..6761b11
Binary files /dev/null and b/contrib/logo3d/logo3d.obj.zip differ
diff --git a/contrib/logo3d/logo3d.wings b/contrib/logo3d/logo3d.wings
new file mode 100644
index 0000000..7db9e15
Binary files /dev/null and b/contrib/logo3d/logo3d.wings differ
diff --git a/contrib/model/examples/euclid.py b/contrib/model/examples/euclid.py
new file mode 100644
index 0000000..028a9e4
--- /dev/null
+++ b/contrib/model/examples/euclid.py
@@ -0,0 +1,2125 @@
+#!/usr/bin/env python
+#
+# euclid graphics maths module
+#
+# Copyright (c) 2006 Alex Holkner
+# Alex.Holkner@mail.google.com
+#
+# This library is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 2.1 of the License, or (at your
+# option) any later version.
+#
+# This library is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
+# for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
+
+'''euclid graphics maths module
+
+Documentation and tests are included in the file "euclid.txt", or online
+at http://code.google.com/p/pyeuclid
+'''
+
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+__revision__ = '$Revision$'
+
+import math
+import operator
+import types
+
+try:
+    import itertools.imap as map  # XXX  Python3 has changed map behavior
+except ImportError:
+    pass
+
+# Some magic here.  If _use_slots is True, the classes will derive from
+# object and will define a __slots__ class variable.  If _use_slots is
+# False, classes will be old-style and will not define __slots__.
+#
+# _use_slots = True:   Memory efficient, probably faster in future versions
+#                      of Python, "better".
+# _use_slots = False:  Ordinary classes, much faster than slots in current
+#                      versions of Python (2.4 and 2.5).
+_use_slots = True
+
+# If True, allows components of Vector2 and Vector3 to be set via swizzling;
+# e.g.  v.xyz = (1, 2, 3).  This is much, much slower than the more verbose
+# v.x = 1; v.y = 2; v.z = 3,  and slows down ordinary element setting as
+# well.  Recommended setting is False.
+_enable_swizzle_set = False
+
+# Requires class to derive from object.
+if _enable_swizzle_set:
+    _use_slots = True
+
+
+# Implement _use_slots magic.
+class _EuclidMetaclass(type):
+    def __new__(cls, name, bases, dct):
+        if '__slots__' in dct:
+            dct['__getstate__'] = cls._create_getstate(dct['__slots__'])
+            dct['__setstate__'] = cls._create_setstate(dct['__slots__'])
+        if _use_slots:
+            return type.__new__(cls, name, bases + (object,), dct)
+        else:
+            if '__slots__' in dct:
+                del dct['__slots__']
+            return types.ClassType.__new__(type, name, bases, dct)
+
+    @classmethod
+    def _create_getstate(cls, slots):
+        def __getstate__(self):
+            d = {}
+            for slot in slots:
+                d[slot] = getattr(self, slot)
+            return d
+
+        return __getstate__
+
+    @classmethod
+    def _create_setstate(cls, slots):
+        def __setstate__(self, state):
+            for name, value in list(state.items()):
+                setattr(self, name, value)
+
+        return __setstate__
+
+
+__metaclass__ = _EuclidMetaclass
+
+
+class Vector2:
+    __slots__ = ['x', 'y']
+
+    def __init__(self, x, y):
+        self.x = x
+        self.y = y
+
+    def __copy__(self):
+        return self.__class__(self.x, self.y)
+
+    copy = __copy__
+
+    def __repr__(self):
+        return 'Vector2(%.2f, %.2f)' % (self.x, self.y)
+
+    def __eq__(self, other):
+        if isinstance(other, Vector2):
+            return self.x == other.x and \
+                   self.y == other.y
+        else:
+            assert hasattr(other, '__len__') and len(other) == 2
+            return self.x == other[0] and \
+                   self.y == other[1]
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __bool__(self):
+        return self.x != 0 or self.y != 0
+
+    def __len__(self):
+        return 2
+
+    def __getitem__(self, key):
+        return (self.x, self.y)[key]
+
+    def __setitem__(self, key, value):
+        l = [self.x, self.y]
+        l[key] = value
+        self.x, self.y = l
+
+    def __iter__(self):
+        return iter((self.x, self.y))
+
+    def __getattr__(self, name):
+        try:
+            return tuple([(self.x, self.y)['xy'.index(c)] \
+                          for c in name])
+        except ValueError:
+            raise AttributeError(name)
+
+    if _enable_swizzle_set:
+        # This has detrimental performance on ordinary setattr as well
+        # if enabled
+        def __setattr__(self, name, value):
+            if len(name) == 1:
+                object.__setattr__(self, name, value)
+            else:
+                try:
+                    l = [self.x, self.y]
+                    for c, v in map(None, name, value):
+                        l['xy'.index(c)] = v
+                    self.x, self.y = l
+                except ValueError:
+                    raise AttributeError(name)
+
+    def __add__(self, other):
+        if isinstance(other, Vector2):
+            return Vector2(self.x + other.x,
+                           self.y + other.y)
+        else:
+            assert hasattr(other, '__len__') and len(other) == 2
+            return Vector2(self.x + other[0],
+                           self.y + other[1])
+
+    __radd__ = __add__
+
+    def __iadd__(self, other):
+        if isinstance(other, Vector2):
+            self.x += other.x
+            self.y += other.y
+        else:
+            self.x += other[0]
+            self.y += other[1]
+        return self
+
+    def __sub__(self, other):
+        if isinstance(other, Vector2):
+            return Vector2(self.x - other.x,
+                           self.y - other.y)
+        else:
+            assert hasattr(other, '__len__') and len(other) == 2
+            return Vector2(self.x - other[0],
+                           self.y - other[1])
+
+    def __rsub__(self, other):
+        if isinstance(other, Vector2):
+            return Vector2(other.x - self.x,
+                           other.y - self.y)
+        else:
+            assert hasattr(other, '__len__') and len(other) == 2
+            return Vector2(other.x - self[0],
+                           other.y - self[1])
+
+    def __mul__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector2(self.x * other,
+                       self.y * other)
+
+    __rmul__ = __mul__
+
+    def __imul__(self, other):
+        assert type(other) in (int, int, float)
+        self.x *= other
+        self.y *= other
+        return self
+
+    def __div__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector2(operator.div(self.x, other),
+                       operator.div(self.y, other))
+
+    def __rdiv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector2(operator.div(other, self.x),
+                       operator.div(other, self.y))
+
+    def __floordiv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector2(operator.floordiv(self.x, other),
+                       operator.floordiv(self.y, other))
+
+    def __rfloordiv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector2(operator.floordiv(other, self.x),
+                       operator.floordiv(other, self.y))
+
+    def __truediv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector2(operator.truediv(self.x, other),
+                       operator.truediv(self.y, other))
+
+    def __rtruediv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector2(operator.truediv(other, self.x),
+                       operator.truediv(other, self.y))
+
+    def __neg__(self):
+        return Vector2(-self.x,
+                       -self.y)
+
+    __pos__ = __copy__
+
+    def __abs__(self):
+        return math.sqrt(self.x ** 2 + \
+                         self.y ** 2)
+
+    magnitude = __abs__
+
+    def magnitude_squared(self):
+        return self.x ** 2 + \
+               self.y ** 2
+
+    def normalize(self):
+        d = self.magnitude()
+        if d:
+            self.x /= d
+            self.y /= d
+        return self
+
+    def normalized(self):
+        d = self.magnitude()
+        if d:
+            return Vector2(self.x / d,
+                           self.y / d)
+        return self.copy()
+
+    def dot(self, other):
+        assert isinstance(other, Vector2)
+        return self.x * other.x + \
+               self.y * other.y
+
+    def cross(self):
+        return Vector2(self.y, -self.x)
+
+    def reflect(self, normal):
+        # assume normal is normalized
+        assert isinstance(normal, Vector2)
+        d = 2 * (self.x * normal.x + self.y * normal.y)
+        return Vector2(self.x - d * normal.x,
+                       self.y - d * normal.y)
+
+
+class Vector3:
+    __slots__ = ['x', 'y', 'z']
+
+    def __init__(self, x, y, z):
+        self.x = x
+        self.y = y
+        self.z = z
+
+    def __copy__(self):
+        return self.__class__(self.x, self.y, self.z)
+
+    copy = __copy__
+
+    def __repr__(self):
+        return 'Vector3(%.2f, %.2f, %.2f)' % (self.x,
+                                              self.y,
+                                              self.z)
+
+    def __eq__(self, other):
+        if isinstance(other, Vector3):
+            return self.x == other.x and \
+                   self.y == other.y and \
+                   self.z == other.z
+        else:
+            assert hasattr(other, '__len__') and len(other) == 3
+            return self.x == other[0] and \
+                   self.y == other[1] and \
+                   self.z == other[2]
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __bool__(self):
+        return self.x != 0 or self.y != 0 or self.z != 0
+
+    def __len__(self):
+        return 3
+
+    def __getitem__(self, key):
+        return (self.x, self.y, self.z)[key]
+
+    def __setitem__(self, key, value):
+        l = [self.x, self.y, self.z]
+        l[key] = value
+        self.x, self.y, self.z = l
+
+    def __iter__(self):
+        return iter((self.x, self.y, self.z))
+
+    def __getattr__(self, name):
+        try:
+            return tuple([(self.x, self.y, self.z)['xyz'.index(c)] \
+                          for c in name])
+        except ValueError:
+            raise AttributeError(name)
+
+    if _enable_swizzle_set:
+        # This has detrimental performance on ordinary setattr as well
+        # if enabled
+        def __setattr__(self, name, value):
+            if len(name) == 1:
+                object.__setattr__(self, name, value)
+            else:
+                try:
+                    l = [self.x, self.y, self.z]
+                    for c, v in map(None, name, value):
+                        l['xyz'.index(c)] = v
+                    self.x, self.y, self.z = l
+                except ValueError:
+                    raise AttributeError(name)
+
+    def __add__(self, other):
+        if isinstance(other, Vector3):
+            # Vector + Vector -> Vector
+            # Vector + Point -> Point
+            # Point + Point -> Vector
+            if self.__class__ is other.__class__:
+                _class = Vector3
+            else:
+                _class = Point3
+            return _class(self.x + other.x,
+                          self.y + other.y,
+                          self.z + other.z)
+        else:
+            assert hasattr(other, '__len__') and len(other) == 3
+            return Vector3(self.x + other[0],
+                           self.y + other[1],
+                           self.z + other[2])
+
+    __radd__ = __add__
+
+    def __iadd__(self, other):
+        if isinstance(other, Vector3):
+            self.x += other.x
+            self.y += other.y
+            self.z += other.z
+        else:
+            self.x += other[0]
+            self.y += other[1]
+            self.z += other[2]
+        return self
+
+    def __sub__(self, other):
+        if isinstance(other, Vector3):
+            # Vector - Vector -> Vector
+            # Vector - Point -> Point
+            # Point - Point -> Vector
+            if self.__class__ is other.__class__:
+                _class = Vector3
+            else:
+                _class = Point3
+            return Vector3(self.x - other.x,
+                           self.y - other.y,
+                           self.z - other.z)
+        else:
+            assert hasattr(other, '__len__') and len(other) == 3
+            return Vector3(self.x - other[0],
+                           self.y - other[1],
+                           self.z - other[2])
+
+    def __rsub__(self, other):
+        if isinstance(other, Vector3):
+            return Vector3(other.x - self.x,
+                           other.y - self.y,
+                           other.z - self.z)
+        else:
+            assert hasattr(other, '__len__') and len(other) == 3
+            return Vector3(other.x - self[0],
+                           other.y - self[1],
+                           other.z - self[2])
+
+    def __mul__(self, other):
+        if isinstance(other, Vector3):
+            # TODO component-wise mul/div in-place and on Vector2; docs.
+            if self.__class__ is Point3 or other.__class__ is Point3:
+                _class = Point3
+            else:
+                _class = Vector3
+            return _class(self.x * other.x,
+                          self.y * other.y,
+                          self.z * other.z)
+        else:
+            assert type(other) in (int, int, float)
+            return Vector3(self.x * other,
+                           self.y * other,
+                           self.z * other)
+
+    __rmul__ = __mul__
+
+    def __imul__(self, other):
+        assert type(other) in (int, int, float)
+        self.x *= other
+        self.y *= other
+        self.z *= other
+        return self
+
+    def __div__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector3(operator.div(self.x, other),
+                       operator.div(self.y, other),
+                       operator.div(self.z, other))
+
+    def __rdiv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector3(operator.div(other, self.x),
+                       operator.div(other, self.y),
+                       operator.div(other, self.z))
+
+    def __floordiv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector3(operator.floordiv(self.x, other),
+                       operator.floordiv(self.y, other),
+                       operator.floordiv(self.z, other))
+
+    def __rfloordiv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector3(operator.floordiv(other, self.x),
+                       operator.floordiv(other, self.y),
+                       operator.floordiv(other, self.z))
+
+    def __truediv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector3(operator.truediv(self.x, other),
+                       operator.truediv(self.y, other),
+                       operator.truediv(self.z, other))
+
+    def __rtruediv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector3(operator.truediv(other, self.x),
+                       operator.truediv(other, self.y),
+                       operator.truediv(other, self.z))
+
+    def __neg__(self):
+        return Vector3(-self.x,
+                       -self.y,
+                       -self.z)
+
+    __pos__ = __copy__
+
+    def __abs__(self):
+        return math.sqrt(self.x ** 2 + \
+                         self.y ** 2 + \
+                         self.z ** 2)
+
+    magnitude = __abs__
+
+    def magnitude_squared(self):
+        return self.x ** 2 + \
+               self.y ** 2 + \
+               self.z ** 2
+
+    def normalize(self):
+        d = self.magnitude()
+        if d:
+            self.x /= d
+            self.y /= d
+            self.z /= d
+        return self
+
+    def normalized(self):
+        d = self.magnitude()
+        if d:
+            return Vector3(self.x / d,
+                           self.y / d,
+                           self.z / d)
+        return self.copy()
+
+    def dot(self, other):
+        assert isinstance(other, Vector3)
+        return self.x * other.x + \
+               self.y * other.y + \
+               self.z * other.z
+
+    def cross(self, other):
+        assert isinstance(other, Vector3)
+        return Vector3(self.y * other.z - self.z * other.y,
+                       -self.x * other.z + self.z * other.x,
+                       self.x * other.y - self.y * other.x)
+
+    def reflect(self, normal):
+        # assume normal is normalized
+        assert isinstance(normal, Vector3)
+        d = 2 * (self.x * normal.x + self.y * normal.y + self.z * normal.z)
+        return Vector3(self.x - d * normal.x,
+                       self.y - d * normal.y,
+                       self.z - d * normal.z)
+
+
+class AffineVector3(Vector3):
+    w = 1
+
+    def __repr__(self):
+        return 'Vector3(%.2f, %.2f, %.2f, 1.00)' % (self.x,
+                                                    self.y,
+                                                    self.z)
+
+    def __len__(self):
+        return 4
+
+    def __getitem__(self, key):
+        return (self.x, self.y, self.z, 1)[key]
+
+    def __iter__(self):
+        return iter((self.x, self.y, self.z, 1))
+
+
+# a b c
+# e f g
+# i j k
+
+class Matrix3:
+    __slots__ = list('abcefgijk')
+
+    def __init__(self):
+        self.identity()
+
+    def __copy__(self):
+        M = Matrix3()
+        M.a = self.a
+        M.b = self.b
+        M.c = self.c
+        M.e = self.e
+        M.f = self.f
+        M.g = self.g
+        M.i = self.i
+        M.j = self.j
+        M.k = self.k
+        return M
+
+    copy = __copy__
+
+    def __repr__(self):
+        return ('Matrix3([% 8.2f % 8.2f % 8.2f\n' \
+                '         % 8.2f % 8.2f % 8.2f\n' \
+                '         % 8.2f % 8.2f % 8.2f])') \
+               % (self.a, self.b, self.c,
+                  self.e, self.f, self.g,
+                  self.i, self.j, self.k)
+
+    def __getitem__(self, key):
+        return [self.a, self.e, self.i,
+                self.b, self.f, self.j,
+                self.c, self.g, self.k][key]
+
+    def __setitem__(self, key, value):
+        L = self[:]
+        L[key] = value
+        (self.a, self.e, self.i,
+         self.b, self.f, self.j,
+         self.c, self.g, self.k) = L
+
+    def __mul__(self, other):
+        if isinstance(other, Matrix3):
+            # Caching repeatedly accessed attributes in local variables
+            # apparently increases performance by 20%.  Attrib: Will McGugan.
+            Aa = self.a
+            Ab = self.b
+            Ac = self.c
+            Ae = self.e
+            Af = self.f
+            Ag = self.g
+            Ai = self.i
+            Aj = self.j
+            Ak = self.k
+            Ba = other.a
+            Bb = other.b
+            Bc = other.c
+            Be = other.e
+            Bf = other.f
+            Bg = other.g
+            Bi = other.i
+            Bj = other.j
+            Bk = other.k
+            C = Matrix3()
+            C.a = Aa * Ba + Ab * Be + Ac * Bi
+            C.b = Aa * Bb + Ab * Bf + Ac * Bj
+            C.c = Aa * Bc + Ab * Bg + Ac * Bk
+            C.e = Ae * Ba + Af * Be + Ag * Bi
+            C.f = Ae * Bb + Af * Bf + Ag * Bj
+            C.g = Ae * Bc + Af * Bg + Ag * Bk
+            C.i = Ai * Ba + Aj * Be + Ak * Bi
+            C.j = Ai * Bb + Aj * Bf + Ak * Bj
+            C.k = Ai * Bc + Aj * Bg + Ak * Bk
+            return C
+        elif isinstance(other, Point2):
+            A = self
+            B = other
+            P = Point2(0, 0)
+            P.x = A.a * B.x + A.b * B.y + A.c
+            P.y = A.e * B.x + A.f * B.y + A.g
+            return P
+        elif isinstance(other, Vector2):
+            A = self
+            B = other
+            V = Vector2(0, 0)
+            V.x = A.a * B.x + A.b * B.y
+            V.y = A.e * B.x + A.f * B.y
+            return V
+        else:
+            other = other.copy()
+            other._apply_transform(self)
+            return other
+
+    def __imul__(self, other):
+        assert isinstance(other, Matrix3)
+        # Cache attributes in local vars (see Matrix3.__mul__).
+        Aa = self.a
+        Ab = self.b
+        Ac = self.c
+        Ae = self.e
+        Af = self.f
+        Ag = self.g
+        Ai = self.i
+        Aj = self.j
+        Ak = self.k
+        Ba = other.a
+        Bb = other.b
+        Bc = other.c
+        Be = other.e
+        Bf = other.f
+        Bg = other.g
+        Bi = other.i
+        Bj = other.j
+        Bk = other.k
+        self.a = Aa * Ba + Ab * Be + Ac * Bi
+        self.b = Aa * Bb + Ab * Bf + Ac * Bj
+        self.c = Aa * Bc + Ab * Bg + Ac * Bk
+        self.e = Ae * Ba + Af * Be + Ag * Bi
+        self.f = Ae * Bb + Af * Bf + Ag * Bj
+        self.g = Ae * Bc + Af * Bg + Ag * Bk
+        self.i = Ai * Ba + Aj * Be + Ak * Bi
+        self.j = Ai * Bb + Aj * Bf + Ak * Bj
+        self.k = Ai * Bc + Aj * Bg + Ak * Bk
+        return self
+
+    def identity(self):
+        self.a = self.f = self.k = 1.
+        self.b = self.c = self.e = self.g = self.i = self.j = 0
+        return self
+
+    def scale(self, x, y):
+        self *= Matrix3.new_scale(x, y)
+        return self
+
+    def translate(self, x, y):
+        self *= Matrix3.new_translate(x, y)
+        return self
+
+    def rotate(self, angle):
+        self *= Matrix3.new_rotate(angle)
+        return self
+
+    # Static constructors
+    def new_identity(cls):
+        self = cls()
+        return self
+
+    new_identity = classmethod(new_identity)
+
+    def new_scale(cls, x, y):
+        self = cls()
+        self.a = x
+        self.f = y
+        return self
+
+    new_scale = classmethod(new_scale)
+
+    def new_translate(cls, x, y):
+        self = cls()
+        self.c = x
+        self.g = y
+        return self
+
+    new_translate = classmethod(new_translate)
+
+    def new_rotate(cls, angle):
+        self = cls()
+        s = math.sin(angle)
+        c = math.cos(angle)
+        self.a = self.f = c
+        self.b = -s
+        self.e = s
+        return self
+
+    new_rotate = classmethod(new_rotate)
+
+
+# a b c d
+# e f g h
+# i j k l
+# m n o p
+
+class Matrix4:
+    __slots__ = list('abcdefghijklmnop')
+
+    def __init__(self):
+        self.identity()
+
+    def __copy__(self):
+        M = Matrix4()
+        M.a = self.a
+        M.b = self.b
+        M.c = self.c
+        M.d = self.d
+        M.e = self.e
+        M.f = self.f
+        M.g = self.g
+        M.h = self.h
+        M.i = self.i
+        M.j = self.j
+        M.k = self.k
+        M.l = self.l
+        M.m = self.m
+        M.n = self.n
+        M.o = self.o
+        M.p = self.p
+        return M
+
+    copy = __copy__
+
+    def __repr__(self):
+        return ('Matrix4([% 8.2f % 8.2f % 8.2f % 8.2f\n' \
+                '         % 8.2f % 8.2f % 8.2f % 8.2f\n' \
+                '         % 8.2f % 8.2f % 8.2f % 8.2f\n' \
+                '         % 8.2f % 8.2f % 8.2f % 8.2f])') \
+               % (self.a, self.b, self.c, self.d,
+                  self.e, self.f, self.g, self.h,
+                  self.i, self.j, self.k, self.l,
+                  self.m, self.n, self.o, self.p)
+
+    def __getitem__(self, key):
+        return [self.a, self.e, self.i, self.m,
+                self.b, self.f, self.j, self.n,
+                self.c, self.g, self.k, self.o,
+                self.d, self.h, self.l, self.p][key]
+
+    def __setitem__(self, key, value):
+        assert not isinstance(key, slice) or \
+               key.stop - key.start == len(value), 'key length != value length'
+        L = self[:]
+        L[key] = value
+        (self.a, self.e, self.i, self.m,
+         self.b, self.f, self.j, self.n,
+         self.c, self.g, self.k, self.o,
+         self.d, self.h, self.l, self.p) = L
+
+    def __mul__(self, other):
+        if isinstance(other, Matrix4):
+            # Cache attributes in local vars (see Matrix3.__mul__).
+            Aa = self.a
+            Ab = self.b
+            Ac = self.c
+            Ad = self.d
+            Ae = self.e
+            Af = self.f
+            Ag = self.g
+            Ah = self.h
+            Ai = self.i
+            Aj = self.j
+            Ak = self.k
+            Al = self.l
+            Am = self.m
+            An = self.n
+            Ao = self.o
+            Ap = self.p
+            Ba = other.a
+            Bb = other.b
+            Bc = other.c
+            Bd = other.d
+            Be = other.e
+            Bf = other.f
+            Bg = other.g
+            Bh = other.h
+            Bi = other.i
+            Bj = other.j
+            Bk = other.k
+            Bl = other.l
+            Bm = other.m
+            Bn = other.n
+            Bo = other.o
+            Bp = other.p
+            C = Matrix4()
+            C.a = Aa * Ba + Ab * Be + Ac * Bi + Ad * Bm
+            C.b = Aa * Bb + Ab * Bf + Ac * Bj + Ad * Bn
+            C.c = Aa * Bc + Ab * Bg + Ac * Bk + Ad * Bo
+            C.d = Aa * Bd + Ab * Bh + Ac * Bl + Ad * Bp
+            C.e = Ae * Ba + Af * Be + Ag * Bi + Ah * Bm
+            C.f = Ae * Bb + Af * Bf + Ag * Bj + Ah * Bn
+            C.g = Ae * Bc + Af * Bg + Ag * Bk + Ah * Bo
+            C.h = Ae * Bd + Af * Bh + Ag * Bl + Ah * Bp
+            C.i = Ai * Ba + Aj * Be + Ak * Bi + Al * Bm
+            C.j = Ai * Bb + Aj * Bf + Ak * Bj + Al * Bn
+            C.k = Ai * Bc + Aj * Bg + Ak * Bk + Al * Bo
+            C.l = Ai * Bd + Aj * Bh + Ak * Bl + Al * Bp
+            C.m = Am * Ba + An * Be + Ao * Bi + Ap * Bm
+            C.n = Am * Bb + An * Bf + Ao * Bj + Ap * Bn
+            C.o = Am * Bc + An * Bg + Ao * Bk + Ap * Bo
+            C.p = Am * Bd + An * Bh + Ao * Bl + Ap * Bp
+            return C
+        elif isinstance(other, Point3):
+            A = self
+            B = other
+            P = Point3(0, 0, 0)
+            P.x = A.a * B.x + A.b * B.y + A.c * B.z + A.d
+            P.y = A.e * B.x + A.f * B.y + A.g * B.z + A.h
+            P.z = A.i * B.x + A.j * B.y + A.k * B.z + A.l
+            return P
+        elif isinstance(other, AffineVector3):
+            A = self
+            B = other
+            V = AffineVector3(0, 0, 0)
+            V.x = A.a * B.x + A.b * B.y + A.c * B.z + A.d * B.w
+            V.y = A.e * B.x + A.f * B.y + A.g * B.z + A.h * B.w
+            V.z = A.i * B.x + A.j * B.y + A.k * B.z + A.l * B.w
+            return V
+        elif isinstance(other, Vector3):
+            A = self
+            B = other
+            V = Vector3(0, 0, 0)
+            V.x = A.a * B.x + A.b * B.y + A.c * B.z
+            V.y = A.e * B.x + A.f * B.y + A.g * B.z
+            V.z = A.i * B.x + A.j * B.y + A.k * B.z
+            return V
+        else:
+            other = other.copy()
+            other._apply_transform(self)
+            return other
+
+    def __imul__(self, other):
+        assert isinstance(other, Matrix4)
+        # Cache attributes in local vars (see Matrix3.__mul__).
+        Aa = self.a
+        Ab = self.b
+        Ac = self.c
+        Ad = self.d
+        Ae = self.e
+        Af = self.f
+        Ag = self.g
+        Ah = self.h
+        Ai = self.i
+        Aj = self.j
+        Ak = self.k
+        Al = self.l
+        Am = self.m
+        An = self.n
+        Ao = self.o
+        Ap = self.p
+        Ba = other.a
+        Bb = other.b
+        Bc = other.c
+        Bd = other.d
+        Be = other.e
+        Bf = other.f
+        Bg = other.g
+        Bh = other.h
+        Bi = other.i
+        Bj = other.j
+        Bk = other.k
+        Bl = other.l
+        Bm = other.m
+        Bn = other.n
+        Bo = other.o
+        Bp = other.p
+        self.a = Aa * Ba + Ab * Be + Ac * Bi + Ad * Bm
+        self.b = Aa * Bb + Ab * Bf + Ac * Bj + Ad * Bn
+        self.c = Aa * Bc + Ab * Bg + Ac * Bk + Ad * Bo
+        self.d = Aa * Bd + Ab * Bh + Ac * Bl + Ad * Bp
+        self.e = Ae * Ba + Af * Be + Ag * Bi + Ah * Bm
+        self.f = Ae * Bb + Af * Bf + Ag * Bj + Ah * Bn
+        self.g = Ae * Bc + Af * Bg + Ag * Bk + Ah * Bo
+        self.h = Ae * Bd + Af * Bh + Ag * Bl + Ah * Bp
+        self.i = Ai * Ba + Aj * Be + Ak * Bi + Al * Bm
+        self.j = Ai * Bb + Aj * Bf + Ak * Bj + Al * Bn
+        self.k = Ai * Bc + Aj * Bg + Ak * Bk + Al * Bo
+        self.l = Ai * Bd + Aj * Bh + Ak * Bl + Al * Bp
+        self.m = Am * Ba + An * Be + Ao * Bi + Ap * Bm
+        self.n = Am * Bb + An * Bf + Ao * Bj + Ap * Bn
+        self.o = Am * Bc + An * Bg + Ao * Bk + Ap * Bo
+        self.p = Am * Bd + An * Bh + Ao * Bl + Ap * Bp
+        return self
+
+    def identity(self):
+        self.a = self.f = self.k = self.p = 1.
+        self.b = self.c = self.d = self.e = self.g = self.h = \
+            self.i = self.j = self.l = self.m = self.n = self.o = 0
+        return self
+
+    def scale(self, x, y, z):
+        self *= Matrix4.new_scale(x, y, z)
+        return self
+
+    def translate(self, x, y, z):
+        self *= Matrix4.new_translate(x, y, z)
+        return self
+
+    def rotatex(self, angle):
+        self *= Matrix4.new_rotatex(angle)
+        return self
+
+    def rotatey(self, angle):
+        self *= Matrix4.new_rotatey(angle)
+        return self
+
+    def rotatez(self, angle):
+        self *= Matrix4.new_rotatez(angle)
+        return self
+
+    def rotate_axis(self, angle, axis):
+        self *= Matrix4.new_rotate_axis(angle, axis)
+        return self
+
+    def rotate_euler(self, heading, attitude, bank):
+        self *= Matrix4.new_rotate_euler(heading, attitude, bank)
+        return self
+
+    # Static constructors
+    def new_identity(cls):
+        self = cls()
+        return self
+
+    new_identity = classmethod(new_identity)
+
+    def new_scale(cls, x, y, z):
+        self = cls()
+        self.a = x
+        self.f = y
+        self.k = z
+        return self
+
+    new_scale = classmethod(new_scale)
+
+    def new_translate(cls, x, y, z):
+        self = cls()
+        self.d = x
+        self.h = y
+        self.l = z
+        return self
+
+    new_translate = classmethod(new_translate)
+
+    def new_rotatex(cls, angle):
+        self = cls()
+        s = math.sin(angle)
+        c = math.cos(angle)
+        self.f = self.k = c
+        self.g = -s
+        self.j = s
+        return self
+
+    new_rotatex = classmethod(new_rotatex)
+
+    def new_rotatey(cls, angle):
+        self = cls()
+        s = math.sin(angle)
+        c = math.cos(angle)
+        self.a = self.k = c
+        self.c = s
+        self.i = -s
+        return self
+
+    new_rotatey = classmethod(new_rotatey)
+
+    def new_rotatez(cls, angle):
+        self = cls()
+        s = math.sin(angle)
+        c = math.cos(angle)
+        self.a = self.f = c
+        self.b = -s
+        self.e = s
+        return self
+
+    new_rotatez = classmethod(new_rotatez)
+
+    def new_rotate_axis(cls, angle, axis):
+        assert (isinstance(axis, Vector3))
+        vector = axis.normalized()
+        x = vector.x
+        y = vector.y
+        z = vector.z
+
+        self = cls()
+        s = math.sin(angle)
+        c = math.cos(angle)
+        c1 = 1. - c
+
+        # from the glRotate man page
+        self.a = x * x * c1 + c
+        self.b = x * y * c1 - z * s
+        self.c = x * z * c1 + y * s
+        self.e = y * x * c1 + z * s
+        self.f = y * y * c1 + c
+        self.g = y * z * c1 - x * s
+        self.i = x * z * c1 - y * s
+        self.j = y * z * c1 + x * s
+        self.k = z * z * c1 + c
+        return self
+
+    new_rotate_axis = classmethod(new_rotate_axis)
+
+    def new_rotate_euler(cls, heading, attitude, bank):
+        # from http://www.euclideanspace.com/
+        ch = math.cos(heading)
+        sh = math.sin(heading)
+        ca = math.cos(attitude)
+        sa = math.sin(attitude)
+        cb = math.cos(bank)
+        sb = math.sin(bank)
+
+        self = cls()
+        self.a = ch * ca
+        self.b = sh * sb - ch * sa * cb
+        self.c = ch * sa * sb + sh * cb
+        self.e = sa
+        self.f = ca * cb
+        self.g = -ca * sb
+        self.i = -sh * ca
+        self.j = sh * sa * cb + ch * sb
+        self.k = -sh * sa * sb + ch * cb
+        return self
+
+    new_rotate_euler = classmethod(new_rotate_euler)
+
+    def new_perspective(cls, fov_y, aspect, near, far):
+        # from the gluPerspective man page
+        f = 1 / math.tan(fov_y / 2)
+        self = cls()
+        assert near != 0.0 and near != far
+        self.a = f / aspect
+        self.f = f
+        self.k = (far + near) / (near - far)
+        self.l = 2 * far * near / (near - far)
+        self.o = -1
+        self.p = 0
+        return self
+
+    new_perspective = classmethod(new_perspective)
+
+
+class Quaternion:
+    # All methods and naming conventions based off
+    # http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions
+
+    # w is the real part, (x, y, z) are the imaginary parts
+    __slots__ = ['w', 'x', 'y', 'z']
+
+    def __init__(self):
+        self.identity()
+
+    def __copy__(self):
+        Q = Quaternion()
+        Q.w = self.w
+        Q.x = self.x
+        Q.y = self.y
+        Q.z = self.z
+
+    copy = __copy__
+
+    def __repr__(self):
+        return 'Quaternion(real=%.2f, imag=<%.2f, %.2f, %.2f>)' % \
+               (self.w, self.x, self.y, self.z)
+
+    def __mul__(self, other):
+        if isinstance(other, Quaternion):
+            Ax = self.x
+            Ay = self.y
+            Az = self.z
+            Aw = self.w
+            Bx = other.x
+            By = other.y
+            Bz = other.z
+            Bw = other.w
+            Q = Quaternion()
+            Q.x = Ax * Bw + Ay * Bz - Az * By + Aw * Bx
+            Q.y = -Ax * Bz + Ay * Bw + Az * Bx + Aw * By
+            Q.z = Ax * By - Ay * Bx + Az * Bw + Aw * Bz
+            Q.w = -Ax * Bx - Ay * By - Az * Bz + Aw * Bw
+            return Q
+        elif isinstance(other, Vector3):
+            w = self.w
+            x = self.x
+            y = self.y
+            z = self.z
+            Vx = other.x
+            Vy = other.y
+            Vz = other.z
+            return other.__class__( \
+                w * w * Vx + 2 * y * w * Vz - 2 * z * w * Vy + \
+                x * x * Vx + 2 * y * x * Vy + 2 * z * x * Vz - \
+                z * z * Vx - y * y * Vx,
+                2 * x * y * Vx + y * y * Vy + 2 * z * y * Vz + \
+                2 * w * z * Vx - z * z * Vy + w * w * Vy - \
+                2 * x * w * Vz - x * x * Vy,
+                2 * x * z * Vx + 2 * y * z * Vy + \
+                z * z * Vz - 2 * w * y * Vx - y * y * Vz + \
+                2 * w * x * Vy - x * x * Vz + w * w * Vz)
+        else:
+            other = other.copy()
+            other._apply_transform(self)
+            return other
+
+    def __imul__(self, other):
+        assert isinstance(other, Quaternion)
+        Ax = self.x
+        Ay = self.y
+        Az = self.z
+        Aw = self.w
+        Bx = other.x
+        By = other.y
+        Bz = other.z
+        Bw = other.w
+        self.x = Ax * Bw + Ay * Bz - Az * By + Aw * Bx
+        self.y = -Ax * Bz + Ay * Bw + Az * Bx + Aw * By
+        self.z = Ax * By - Ay * Bx + Az * Bw + Aw * Bz
+        self.w = -Ax * Bx - Ay * By - Az * Bz + Aw * Bw
+        return self
+
+    def __abs__(self):
+        return math.sqrt(self.w ** 2 + \
+                         self.x ** 2 + \
+                         self.y ** 2 + \
+                         self.z ** 2)
+
+    magnitude = __abs__
+
+    def magnitude_squared(self):
+        return self.w ** 2 + \
+               self.x ** 2 + \
+               self.y ** 2 + \
+               self.z ** 2
+
+    def identity(self):
+        self.w = 1
+        self.x = 0
+        self.y = 0
+        self.z = 0
+        return self
+
+    def rotate_axis(self, angle, axis):
+        self *= Quaternion.new_rotate_axis(angle, axis)
+        return self
+
+    def rotate_euler(self, heading, attitude, bank):
+        self *= Quaternion.new_rotate_euler(heading, attitude, bank)
+        return self
+
+    def conjugated(self):
+        Q = Quaternion()
+        Q.w = self.w
+        Q.x = -self.x
+        Q.y = -self.y
+        Q.z = -self.z
+        return Q
+
+    def normalize(self):
+        d = self.magnitude()
+        if d != 0:
+            self.w /= d
+            self.x /= d
+            self.y /= d
+            self.z /= d
+        return self
+
+    def normalized(self):
+        d = self.magnitude()
+        if d != 0:
+            Q = Quaternion()
+            Q.w /= d
+            Q.x /= d
+            Q.y /= d
+            Q.z /= d
+            return Q
+        else:
+            return self.copy()
+
+    def get_angle_axis(self):
+        if self.w > 1:
+            self = self.normalized()
+        angle = 2 * math.acos(self.w)
+        s = math.sqrt(1 - self.w ** 2)
+        if s < 0.001:
+            return angle, Vector3(1, 0, 0)
+        else:
+            return angle, Vector3(self.x / s, self.y / s, self.z / s)
+
+    def get_euler(self):
+        t = self.x * self.y + self.z * self.w
+        if t > 0.4999:
+            heading = 2 * math.atan2(self.x, self.w)
+            attitude = math.pi / 2
+            bank = 0
+        elif t < -0.4999:
+            heading = -2 * math.atan2(self.x, self.w)
+            attitude = -math.pi / 2
+            bank = 0
+        else:
+            sqx = self.x ** 2
+            sqy = self.y ** 2
+            sqz = self.z ** 2
+            heading = math.atan2(2 * self.y * self.w - 2 * self.x * self.z,
+                                 1 - 2 * sqy - 2 * sqz)
+            attitude = math.asin(2 * t)
+            bank = math.atan2(2 * self.x * self.w - 2 * self.y * self.z,
+                              1 - 2 * sqx - 2 * sqz)
+        return heading, attitude, bank
+
+    def get_matrix(self):
+        xx = self.x ** 2
+        xy = self.x * self.y
+        xz = self.x * self.z
+        xw = self.x * self.w
+        yy = self.y ** 2
+        yz = self.y * self.z
+        yw = self.y * self.w
+        zz = self.z ** 2
+        zw = self.z * self.w
+        M = Matrix4()
+        M.a = 1 - 2 * (yy + zz)
+        M.b = 2 * (xy - zw)
+        M.c = 2 * (xz + yw)
+        M.e = 2 * (xy + zw)
+        M.f = 1 - 2 * (xx + zz)
+        M.g = 2 * (yz - xw)
+        M.i = 2 * (xz - yw)
+        M.j = 2 * (yz + xw)
+        M.k = 1 - 2 * (xx + yy)
+        return M
+
+    # Static constructors
+    def new_identity(cls):
+        return cls()
+
+    new_identity = classmethod(new_identity)
+
+    def new_rotate_axis(cls, angle, axis):
+        assert (isinstance(axis, Vector3))
+        axis = axis.normalized()
+        s = math.sin(angle / 2)
+        Q = cls()
+        Q.w = math.cos(angle / 2)
+        Q.x = axis.x * s
+        Q.y = axis.y * s
+        Q.z = axis.z * s
+        return Q
+
+    new_rotate_axis = classmethod(new_rotate_axis)
+
+    def new_rotate_euler(cls, heading, attitude, bank):
+        Q = cls()
+        c1 = math.cos(heading / 2)
+        s1 = math.sin(heading / 2)
+        c2 = math.cos(attitude / 2)
+        s2 = math.sin(attitude / 2)
+        c3 = math.cos(bank / 2)
+        s3 = math.sin(bank / 2)
+
+        Q.w = c1 * c2 * c3 - s1 * s2 * s3
+        Q.x = s1 * s2 * c3 + c1 * c2 * s3
+        Q.y = s1 * c2 * c3 + c1 * s2 * s3
+        Q.z = c1 * s2 * c3 - s1 * c2 * s3
+        return Q
+
+    new_rotate_euler = classmethod(new_rotate_euler)
+
+    def new_interpolate(cls, q1, q2, t):
+        assert isinstance(q1, Quaternion) and isinstance(q2, Quaternion)
+        Q = cls()
+
+        costheta = q1.w * q2.w + q1.x * q2.x + q1.y * q2.y + q1.z * q2.z
+        theta = math.acos(costheta)
+        if abs(theta) < 0.01:
+            Q.w = q2.w
+            Q.x = q2.x
+            Q.y = q2.y
+            Q.z = q2.z
+            return Q
+
+        sintheta = math.sqrt(1.0 - costheta * costheta)
+        if abs(sintheta) < 0.01:
+            Q.w = (q1.w + q2.w) * 0.5
+            Q.x = (q1.x + q2.x) * 0.5
+            Q.y = (q1.y + q2.y) * 0.5
+            Q.z = (q1.z + q2.z) * 0.5
+            return Q
+
+        ratio1 = math.sin((1 - t) * theta) / sintheta
+        ratio2 = math.sin(t * theta) / sintheta
+
+        Q.w = q1.w * ratio1 + q2.w * ratio2
+        Q.x = q1.x * ratio1 + q2.x * ratio2
+        Q.y = q1.y * ratio1 + q2.y * ratio2
+        Q.z = q1.z * ratio1 + q2.z * ratio2
+        return Q
+
+    new_interpolate = classmethod(new_interpolate)
+
+
+# Geometry
+# Much maths thanks to Paul Bourke, http://astronomy.swin.edu.au/~pbourke
+# ---------------------------------------------------------------------------
+
+class Geometry:
+    def _connect_unimplemented(self, other):
+        raise AttributeError('Cannot connect %s to %s' % \
+                             (self.__class__, other.__class__))
+
+    def _intersect_unimplemented(self, other):
+        raise AttributeError('Cannot intersect %s and %s' % \
+                             (self.__class__, other.__class__))
+
+    _intersect_point2 = _intersect_unimplemented
+    _intersect_line2 = _intersect_unimplemented
+    _intersect_circle = _intersect_unimplemented
+    _connect_point2 = _connect_unimplemented
+    _connect_line2 = _connect_unimplemented
+    _connect_circle = _connect_unimplemented
+
+    _intersect_point3 = _intersect_unimplemented
+    _intersect_line3 = _intersect_unimplemented
+    _intersect_sphere = _intersect_unimplemented
+    _intersect_plane = _intersect_unimplemented
+    _connect_point3 = _connect_unimplemented
+    _connect_line3 = _connect_unimplemented
+    _connect_sphere = _connect_unimplemented
+    _connect_plane = _connect_unimplemented
+
+    def intersect(self, other):
+        raise NotImplementedError
+
+    def connect(self, other):
+        raise NotImplementedError
+
+    def distance(self, other):
+        c = self.connect(other)
+        if c:
+            return c.length
+        return 0.0
+
+
+def _intersect_point2_circle(P, C):
+    return abs(P - C.c) <= C.r
+
+
+def _intersect_line2_line2(A, B):
+    d = B.v.y * A.v.x - B.v.x * A.v.y
+    if d == 0:
+        return None
+
+    dy = A.p.y - B.p.y
+    dx = A.p.x - B.p.x
+    ua = (B.v.x * dy - B.v.y * dx) / d
+    if not A._u_in(ua):
+        return None
+    ub = (A.v.x * dy - A.v.y * dx) / d
+    if not B._u_in(ub):
+        return None
+
+    return Point2(A.p.x + ua * A.v.x,
+                  A.p.y + ua * A.v.y)
+
+
+def _intersect_line2_circle(L, C):
+    a = L.v.magnitude_squared()
+    b = 2 * (L.v.x * (L.p.x - C.c.x) + \
+             L.v.y * (L.p.y - C.c.y))
+    c = C.c.magnitude_squared() + \
+        L.p.magnitude_squared() - \
+        2 * C.c.dot(L.p) - \
+        C.r ** 2
+    det = b ** 2 - 4 * a * c
+    if det < 0:
+        return None
+    sq = math.sqrt(det)
+    u1 = (-b + sq) / (2 * a)
+    u2 = (-b - sq) / (2 * a)
+    if not L._u_in(u1):
+        u1 = max(min(u1, 1.0), 0.0)
+    if not L._u_in(u2):
+        u2 = max(min(u2, 1.0), 0.0)
+    return LineSegment2(Point2(L.p.x + u1 * L.v.x,
+                               L.p.y + u1 * L.v.y),
+                        Point2(L.p.x + u2 * L.v.x,
+                               L.p.y + u2 * L.v.y))
+
+
+def _connect_point2_line2(P, L):
+    d = L.v.magnitude_squared()
+    assert d != 0
+    u = ((P.x - L.p.x) * L.v.x + \
+         (P.y - L.p.y) * L.v.y) / d
+    if not L._u_in(u):
+        u = max(min(u, 1.0), 0.0)
+    return LineSegment2(P,
+                        Point2(L.p.x + u * L.v.x,
+                               L.p.y + u * L.v.y))
+
+
+def _connect_point2_circle(P, C):
+    v = P - C.c
+    v.normalize()
+    v *= C.r
+    return LineSegment2(P, Point2(C.c.x + v.x, C.c.y + v.y))
+
+
+def _connect_line2_line2(A, B):
+    d = B.v.y * A.v.x - B.v.x * A.v.y
+    if d == 0:
+        # Parallel, connect an endpoint with a line
+        if isinstance(B, Ray2) or isinstance(B, LineSegment2):
+            p1, p2 = _connect_point2_line2(B.p, A)
+            return p2, p1
+        # No endpoint (or endpoint is on A), possibly choose arbitrary point
+        # on line.
+        return _connect_point2_line2(A.p, B)
+
+    dy = A.p.y - B.p.y
+    dx = A.p.x - B.p.x
+    ua = (B.v.x * dy - B.v.y * dx) / d
+    if not A._u_in(ua):
+        ua = max(min(ua, 1.0), 0.0)
+    ub = (A.v.x * dy - A.v.y * dx) / d
+    if not B._u_in(ub):
+        ub = max(min(ub, 1.0), 0.0)
+
+    return LineSegment2(Point2(A.p.x + ua * A.v.x, A.p.y + ua * A.v.y),
+                        Point2(B.p.x + ub * B.v.x, B.p.y + ub * B.v.y))
+
+
+def _connect_circle_line2(C, L):
+    d = L.v.magnitude_squared()
+    assert d != 0
+    u = ((C.c.x - L.p.x) * L.v.x + (C.c.y - L.p.y) * L.v.y) / d
+    if not L._u_in(u):
+        u = max(min(u, 1.0), 0.0)
+    point = Point2(L.p.x + u * L.v.x, L.p.y + u * L.v.y)
+    v = (point - C.c)
+    v.normalize()
+    v *= C.r
+    return LineSegment2(Point2(C.c.x + v.x, C.c.y + v.y), point)
+
+
+def _connect_circle_circle(A, B):
+    v = B.c - A.c
+    v.normalize()
+    return LineSegment2(Point2(A.c.x + v.x * A.r, A.c.y + v.y * A.r),
+                        Point2(B.c.x - v.x * B.r, B.c.y - v.y * B.r))
+
+
+class Point2(Vector2, Geometry):
+    def __repr__(self):
+        return 'Point2(%.2f, %.2f)' % (self.x, self.y)
+
+    def intersect(self, other):
+        return other._intersect_point2(self)
+
+    def _intersect_circle(self, other):
+        return _intersect_point2_circle(self, other)
+
+    def connect(self, other):
+        return other._connect_point2(self)
+
+    def _connect_point2(self, other):
+        return LineSegment2(other, self)
+
+    def _connect_line2(self, other):
+        c = _connect_point2_line2(self, other)
+        if c:
+            return c._swap()
+
+    def _connect_circle(self, other):
+        c = _connect_point2_circle(self, other)
+        if c:
+            return c._swap()
+
+
+class Line2(Geometry):
+    __slots__ = ['p', 'v']
+
+    def __init__(self, *args):
+        if len(args) == 3:
+            assert isinstance(args[0], Point2) and \
+                   isinstance(args[1], Vector2) and \
+                   type(args[2]) == float
+            self.p = args[0].copy()
+            self.v = args[1] * args[2] / abs(args[1])
+        elif len(args) == 2:
+            if isinstance(args[0], Point2) and isinstance(args[1], Point2):
+                self.p = args[0].copy()
+                self.v = args[1] - args[0]
+            elif isinstance(args[0], Point2) and isinstance(args[1], Vector2):
+                self.p = args[0].copy()
+                self.v = args[1].copy()
+            else:
+                raise AttributeError('%r' % (args,))
+        elif len(args) == 1:
+            if isinstance(args[0], Line2):
+                self.p = args[0].p.copy()
+                self.v = args[0].v.copy()
+            else:
+                raise AttributeError('%r' % (args,))
+        else:
+            raise AttributeError('%r' % (args,))
+
+        if not self.v:
+            raise AttributeError('Line has zero-length vector')
+
+    def __copy__(self):
+        return self.__class__(self.p, self.v)
+
+    copy = __copy__
+
+    def __repr__(self):
+        return 'Line2(<%.2f, %.2f> + u<%.2f, %.2f>)' % \
+               (self.p.x, self.p.y, self.v.x, self.v.y)
+
+    p1 = property(lambda self: self.p)
+    p2 = property(lambda self: Point2(self.p.x + self.v.x,
+                                      self.p.y + self.v.y))
+
+    def _apply_transform(self, t):
+        self.p = t * self.p
+        self.v = t * self.v
+
+    def _u_in(self, u):
+        return True
+
+    def intersect(self, other):
+        return other._intersect_line2(self)
+
+    def _intersect_line2(self, other):
+        return _intersect_line2_line2(self, other)
+
+    def _intersect_circle(self, other):
+        return _intersect_line2_circle(self, other)
+
+    def connect(self, other):
+        return other._connect_line2(self)
+
+    def _connect_point2(self, other):
+        return _connect_point2_line2(other, self)
+
+    def _connect_line2(self, other):
+        return _connect_line2_line2(other, self)
+
+    def _connect_circle(self, other):
+        return _connect_circle_line2(other, self)
+
+
+class Ray2(Line2):
+    def __repr__(self):
+        return 'Ray2(<%.2f, %.2f> + u<%.2f, %.2f>)' % \
+               (self.p.x, self.p.y, self.v.x, self.v.y)
+
+    def _u_in(self, u):
+        return u >= 0.0
+
+
+class LineSegment2(Line2):
+    def __repr__(self):
+        return 'LineSegment2(<%.2f, %.2f> to <%.2f, %.2f>)' % \
+               (self.p.x, self.p.y, self.p.x + self.v.x, self.p.y + self.v.y)
+
+    def _u_in(self, u):
+        return u >= 0.0 and u <= 1.0
+
+    def __abs__(self):
+        return abs(self.v)
+
+    def magnitude_squared(self):
+        return self.v.magnitude_squared()
+
+    def _swap(self):
+        # used by connect methods to switch order of points
+        self.p = self.p2
+        self.v *= -1
+        return self
+
+    length = property(lambda self: abs(self.v))
+
+
+class Circle(Geometry):
+    __slots__ = ['c', 'r']
+
+    def __init__(self, center, radius):
+        assert isinstance(center, Vector2) and type(radius) == float
+        self.c = center.copy()
+        self.r = radius
+
+    def __copy__(self):
+        return self.__class__(self.c, self.r)
+
+    copy = __copy__
+
+    def __repr__(self):
+        return 'Circle(<%.2f, %.2f>, radius=%.2f)' % \
+               (self.c.x, self.c.y, self.r)
+
+    def _apply_transform(self, t):
+        self.c = t * self.c
+
+    def intersect(self, other):
+        return other._intersect_circle(self)
+
+    def _intersect_point2(self, other):
+        return _intersect_point2_circle(other, self)
+
+    def _intersect_line2(self, other):
+        return _intersect_line2_circle(other, self)
+
+    def connect(self, other):
+        return other._connect_circle(self)
+
+    def _connect_point2(self, other):
+        return _connect_point2_circle(other, self)
+
+    def _connect_line2(self, other):
+        c = _connect_circle_line2(self, other)
+        if c:
+            return c._swap()
+
+    def _connect_circle(self, other):
+        return _connect_circle_circle(other, self)
+
+
+# 3D Geometry
+# -------------------------------------------------------------------------
+
+def _connect_point3_line3(P, L):
+    d = L.v.magnitude_squared()
+    assert d != 0
+    u = ((P.x - L.p.x) * L.v.x + \
+         (P.y - L.p.y) * L.v.y + \
+         (P.z - L.p.z) * L.v.z) / d
+    if not L._u_in(u):
+        u = max(min(u, 1.0), 0.0)
+    return LineSegment3(P, Point3(L.p.x + u * L.v.x,
+                                  L.p.y + u * L.v.y,
+                                  L.p.z + u * L.v.z))
+
+
+def _connect_point3_sphere(P, S):
+    v = P - S.c
+    v.normalize()
+    v *= S.r
+    return LineSegment3(P, Point3(S.c.x + v.x, S.c.y + v.y, S.c.z + v.z))
+
+
+def _connect_point3_plane(p, plane):
+    n = plane.n.normalized()
+    d = p.dot(plane.n) - plane.k
+    return LineSegment3(p, Point3(p.x - n.x * d, p.y - n.y * d, p.z - n.z * d))
+
+
+def _connect_line3_line3(A, B):
+    assert A.v and B.v
+    p13 = A.p - B.p
+    d1343 = p13.dot(B.v)
+    d4321 = B.v.dot(A.v)
+    d1321 = p13.dot(A.v)
+    d4343 = B.v.magnitude_squared()
+    denom = A.v.magnitude_squared() * d4343 - d4321 ** 2
+    if denom == 0:
+        # Parallel, connect an endpoint with a line
+        if isinstance(B, Ray3) or isinstance(B, LineSegment3):
+            return _connect_point3_line3(B.p, A)._swap()
+        # No endpoint (or endpoint is on A), possibly choose arbitrary
+        # point on line.
+        return _connect_point3_line3(A.p, B)
+
+    ua = (d1343 * d4321 - d1321 * d4343) / denom
+    if not A._u_in(ua):
+        ua = max(min(ua, 1.0), 0.0)
+    ub = (d1343 + d4321 * ua) / d4343
+    if not B._u_in(ub):
+        ub = max(min(ub, 1.0), 0.0)
+    return LineSegment3(Point3(A.p.x + ua * A.v.x,
+                               A.p.y + ua * A.v.y,
+                               A.p.z + ua * A.v.z),
+                        Point3(B.p.x + ub * B.v.x,
+                               B.p.y + ub * B.v.y,
+                               B.p.z + ub * B.v.z))
+
+
+def _connect_line3_plane(L, P):
+    d = P.n.dot(L.v)
+    if not d:
+        # Parallel, choose an endpoint
+        return _connect_point3_plane(L.p, P)
+    u = (P.k - P.n.dot(L.p)) / d
+    if not L._u_in(u):
+        # intersects out of range, choose nearest endpoint
+        u = max(min(u, 1.0), 0.0)
+        return _connect_point3_plane(Point3(L.p.x + u * L.v.x,
+                                            L.p.y + u * L.v.y,
+                                            L.p.z + u * L.v.z), P)
+    # Intersection
+    return None
+
+
+def _connect_sphere_line3(S, L):
+    d = L.v.magnitude_squared()
+    assert d != 0
+    u = ((S.c.x - L.p.x) * L.v.x + \
+         (S.c.y - L.p.y) * L.v.y + \
+         (S.c.z - L.p.z) * L.v.z) / d
+    if not L._u_in(u):
+        u = max(min(u, 1.0), 0.0)
+    point = Point3(L.p.x + u * L.v.x, L.p.y + u * L.v.y, L.p.z + u * L.v.z)
+    v = (point - S.c)
+    v.normalize()
+    v *= S.r
+    return LineSegment3(Point3(S.c.x + v.x, S.c.y + v.y, S.c.z + v.z),
+                        point)
+
+
+def _connect_sphere_sphere(A, B):
+    v = B.c - A.c
+    v.normalize()
+    return LineSegment3(Point3(A.c.x + v.x * A.r,
+                               A.c.y + v.y * A.r,
+                               A.c.x + v.z * A.r),
+                        Point3(B.c.x + v.x * B.r,
+                               B.c.y + v.y * B.r,
+                               B.c.x + v.z * B.r))
+
+
+def _connect_sphere_plane(S, P):
+    c = _connect_point3_plane(S.c, P)
+    if not c:
+        return None
+    p2 = c.p2
+    v = p2 - S.c
+    v.normalize()
+    v *= S.r
+    return LineSegment3(Point3(S.c.x + v.x, S.c.y + v.y, S.c.z + v.z),
+                        p2)
+
+
+def _connect_plane_plane(A, B):
+    if A.n.cross(B.n):
+        # Planes intersect
+        return None
+    else:
+        # Planes are parallel, connect to arbitrary point
+        return _connect_point3_plane(A._get_point(), B)
+
+
+def _intersect_point3_sphere(P, S):
+    return abs(P - S.c) <= S.r
+
+
+def _intersect_line3_sphere(L, S):
+    a = L.v.magnitude_squared()
+    b = 2 * (L.v.x * (L.p.x - S.c.x) + \
+             L.v.y * (L.p.y - S.c.y) + \
+             L.v.z * (L.p.z - S.c.z))
+    c = S.c.magnitude_squared() + \
+        L.p.magnitude_squared() - \
+        2 * S.c.dot(L.p) - \
+        S.r ** 2
+    det = b ** 2 - 4 * a * c
+    if det < 0:
+        return None
+    sq = math.sqrt(det)
+    u1 = (-b + sq) / (2 * a)
+    u2 = (-b - sq) / (2 * a)
+    if not L._u_in(u1):
+        u1 = max(min(u1, 1.0), 0.0)
+    if not L._u_in(u2):
+        u2 = max(min(u2, 1.0), 0.0)
+    return LineSegment3(Point3(L.p.x + u1 * L.v.x,
+                               L.p.y + u1 * L.v.y,
+                               L.p.z + u1 * L.v.z),
+                        Point3(L.p.x + u2 * L.v.x,
+                               L.p.y + u2 * L.v.y,
+                               L.p.z + u2 * L.v.z))
+
+
+def _intersect_line3_plane(L, P):
+    d = P.n.dot(L.v)
+    if not d:
+        # Parallel
+        return None
+    u = (P.k - P.n.dot(L.p)) / d
+    if not L._u_in(u):
+        return None
+    return Point3(L.p.x + u * L.v.x,
+                  L.p.y + u * L.v.y,
+                  L.p.z + u * L.v.z)
+
+
+def _intersect_plane_plane(A, B):
+    n1_m = A.n.magnitude_squared()
+    n2_m = B.n.magnitude_squared()
+    n1d2 = A.n.dot(B.n)
+    det = n1_m * n2_m - n1d2 ** 2
+    if det == 0:
+        # Parallel
+        return None
+    c1 = (A.k * n2_m - B.k * n1d2) / det
+    c2 = (B.k * n1_m - A.k * n1d2) / det
+    return Line3(Point3(c1 * A.n.x + c2 * B.n.x,
+                        c1 * A.n.y + c2 * B.n.y,
+                        c1 * A.n.z + c2 * B.n.z),
+                 A.n.cross(B.n))
+
+
+class Point3(Vector3, Geometry):
+    def __repr__(self):
+        return 'Point3(%.2f, %.2f, %.2f)' % (self.x, self.y, self.z)
+
+    def intersect(self, other):
+        return other._intersect_point3(self)
+
+    def _intersect_sphere(self, other):
+        return _intersect_point3_sphere(self, other)
+
+    def connect(self, other):
+        return other._connect_point3(self)
+
+    def _connect_point3(self, other):
+        if self != other:
+            return LineSegment3(other, self)
+        return None
+
+    def _connect_line3(self, other):
+        c = _connect_point3_line3(self, other)
+        if c:
+            return c._swap()
+
+    def _connect_sphere(self, other):
+        c = _connect_point3_sphere(self, other)
+        if c:
+            return c._swap()
+
+    def _connect_plane(self, other):
+        c = _connect_point3_plane(self, other)
+        if c:
+            return c._swap()
+
+
+class Line3:
+    __slots__ = ['p', 'v']
+
+    def __init__(self, *args):
+        if len(args) == 3:
+            assert isinstance(args[0], Point3) and \
+                   isinstance(args[1], Vector3) and \
+                   type(args[2]) == float
+            self.p = args[0].copy()
+            self.v = args[1] * args[2] / abs(args[1])
+        elif len(args) == 2:
+            if isinstance(args[0], Point3) and isinstance(args[1], Point3):
+                self.p = args[0].copy()
+                self.v = args[1] - args[0]
+            elif isinstance(args[0], Point3) and isinstance(args[1], Vector3):
+                self.p = args[0].copy()
+                self.v = args[1].copy()
+            else:
+                raise AttributeError('%r' % (args,))
+        elif len(args) == 1:
+            if isinstance(args[0], Line3):
+                self.p = args[0].p.copy()
+                self.v = args[0].v.copy()
+            else:
+                raise AttributeError('%r' % (args,))
+        else:
+            raise AttributeError('%r' % (args,))
+
+            # XXX This is annoying.
+            # if not self.v:
+            #    raise AttributeError, 'Line has zero-length vector'
+
+    def __copy__(self):
+        return self.__class__(self.p, self.v)
+
+    copy = __copy__
+
+    def __repr__(self):
+        return 'Line3(<%.2f, %.2f, %.2f> + u<%.2f, %.2f, %.2f>)' % \
+               (self.p.x, self.p.y, self.p.z, self.v.x, self.v.y, self.v.z)
+
+    p1 = property(lambda self: self.p)
+    p2 = property(lambda self: Point3(self.p.x + self.v.x,
+                                      self.p.y + self.v.y,
+                                      self.p.z + self.v.z))
+
+    def _apply_transform(self, t):
+        self.p = t * self.p
+        self.v = t * self.v
+
+    def _u_in(self, u):
+        return True
+
+    def intersect(self, other):
+        return other._intersect_line3(self)
+
+    def _intersect_sphere(self, other):
+        return _intersect_line3_sphere(self, other)
+
+    def _intersect_plane(self, other):
+        return _intersect_line3_plane(self, other)
+
+    def connect(self, other):
+        return other._connect_line3(self)
+
+    def _connect_point3(self, other):
+        return _connect_point3_line3(other, self)
+
+    def _connect_line3(self, other):
+        return _connect_line3_line3(other, self)
+
+    def _connect_sphere(self, other):
+        return _connect_sphere_line3(other, self)
+
+    def _connect_plane(self, other):
+        c = _connect_line3_plane(self, other)
+        if c:
+            return c
+
+
+class Ray3(Line3):
+    def __repr__(self):
+        return 'Ray3(<%.2f, %.2f, %.2f> + u<%.2f, %.2f, %.2f>)' % \
+               (self.p.x, self.p.y, self.p.z, self.v.x, self.v.y, self.v.z)
+
+    def _u_in(self, u):
+        return u >= 0.0
+
+
+class LineSegment3(Line3):
+    def __repr__(self):
+        return 'LineSegment3(<%.2f, %.2f, %.2f> to <%.2f, %.2f, %.2f>)' % \
+               (self.p.x, self.p.y, self.p.z,
+                self.p.x + self.v.x, self.p.y + self.v.y, self.p.z + self.v.z)
+
+    def _u_in(self, u):
+        return u >= 0.0 and u <= 1.0
+
+    def __abs__(self):
+        return abs(self.v)
+
+    def magnitude_squared(self):
+        return self.v.magnitude_squared()
+
+    def _swap(self):
+        # used by connect methods to switch order of points
+        self.p = self.p2
+        self.v *= -1
+        return self
+
+    length = property(lambda self: abs(self.v))
+
+
+class Sphere:
+    __slots__ = ['c', 'r']
+
+    def __init__(self, center, radius):
+        assert isinstance(center, Vector3) and type(radius) == float
+        self.c = center.copy()
+        self.r = radius
+
+    def __copy__(self):
+        return self.__class__(self.c, self.r)
+
+    copy = __copy__
+
+    def __repr__(self):
+        return 'Sphere(<%.2f, %.2f, %.2f>, radius=%.2f)' % \
+               (self.c.x, self.c.y, self.c.z, self.r)
+
+    def _apply_transform(self, t):
+        self.c = t * self.c
+
+    def intersect(self, other):
+        return other._intersect_sphere(self)
+
+    def _intersect_point3(self, other):
+        return _intersect_point3_sphere(other, self)
+
+    def _intersect_line3(self, other):
+        return _intersect_line3_sphere(other, self)
+
+    def connect(self, other):
+        return other._connect_sphere(self)
+
+    def _connect_point3(self, other):
+        return _connect_point3_sphere(other, self)
+
+    def _connect_line3(self, other):
+        c = _connect_sphere_line3(self, other)
+        if c:
+            return c._swap()
+
+    def _connect_sphere(self, other):
+        return _connect_sphere_sphere(other, self)
+
+    def _connect_plane(self, other):
+        c = _connect_sphere_plane(self, other)
+        if c:
+            return c
+
+
+class Plane:
+    # n.p = k, where n is normal, p is point on plane, k is constant scalar
+    __slots__ = ['n', 'k']
+
+    def __init__(self, *args):
+        if len(args) == 3:
+            assert isinstance(args[0], Point3) and \
+                   isinstance(args[1], Point3) and \
+                   isinstance(args[2], Point3)
+            self.n = (args[1] - args[0]).cross(args[2] - args[0])
+            self.n.normalize()
+            self.k = self.n.dot(args[0])
+        elif len(args) == 2:
+            if isinstance(args[0], Point3) and isinstance(args[1], Vector3):
+                self.n = args[1].normalized()
+                self.k = self.n.dot(args[0])
+            elif isinstance(args[0], Vector3) and type(args[1]) == float:
+                self.n = args[0].normalized()
+                self.k = args[1]
+            else:
+                raise AttributeError('%r' % (args,))
+
+        else:
+            raise AttributeError('%r' % (args,))
+
+        if not self.n:
+            raise AttributeError('Points on plane are colinear')
+
+    def __copy__(self):
+        return self.__class__(self.n, self.k)
+
+    copy = __copy__
+
+    def __repr__(self):
+        return 'Plane(<%.2f, %.2f, %.2f>.p = %.2f)' % \
+               (self.n.x, self.n.y, self.n.z, self.k)
+
+    def _get_point(self):
+        # Return an arbitrary point on the plane
+        if self.n.z:
+            return Point3(0., 0., self.k / self.n.z)
+        elif self.n.y:
+            return Point3(0., self.k / self.n.y, 0.)
+        else:
+            return Point3(self.k / self.n.x, 0., 0.)
+
+    def _apply_transform(self, t):
+        p = t * self._get_point()
+        self.n = t * self.n
+        self.k = self.n.dot(p)
+
+    def intersect(self, other):
+        return other._intersect_plane(self)
+
+    def _intersect_line3(self, other):
+        return _intersect_line3_plane(other, self)
+
+    def _intersect_plane(self, other):
+        return _intersect_plane_plane(self, other)
+
+    def connect(self, other):
+        return other._connect_plane(self)
+
+    def _connect_point3(self, other):
+        return _connect_point3_plane(other, self)
+
+    def _connect_line3(self, other):
+        return _connect_line3_plane(other, self)
+
+    def _connect_sphere(self, other):
+        return _connect_sphere_plane(other, self)
+
+    def _connect_plane(self, other):
+        return _connect_plane_plane(other, self)
diff --git a/contrib/model/examples/obj_test.py b/contrib/model/examples/obj_test.py
new file mode 100644
index 0000000..23af560
--- /dev/null
+++ b/contrib/model/examples/obj_test.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python
+
+'''
+'''
+
+__docformat__ = 'restructuredtext'
+__version__ = '$Id: obj_test.py 111 2006-10-20 06:39:12Z r1chardj0n3s $'
+
+import sys
+import os
+import ctypes
+import pyglet
+
+from pyglet.gl import *
+from model import obj
+
+
+w = pyglet.window.Window()
+
+fourfv = ctypes.c_float * 4
+glLightfv(GL_LIGHT0, GL_POSITION, fourfv(10, 20, 20, 0))
+glLightfv(GL_LIGHT0, GL_AMBIENT, fourfv(0.2, 0.2, 0.2, 1.0))
+glLightfv(GL_LIGHT0, GL_DIFFUSE, fourfv(0.8, 0.8, 0.8, 1.0))
+glEnable(GL_LIGHT0)
+glEnable(GL_LIGHTING)
+glEnable(GL_DEPTH_TEST)
+
+@w.event
+def on_resize(width, height):
+    glMatrixMode(GL_PROJECTION)
+    glLoadIdentity()
+    gluPerspective(60., float(width)/height, 1., 100.)
+    glMatrixMode(GL_MODELVIEW)
+    return True
+
+@w.event
+def on_draw():
+    w.clear()
+    glLoadIdentity()
+    gluLookAt(0, 3, 3, 0, 0, 0, 0, 1, 0)
+    glRotatef(r, 0, 1, 0)
+    glRotatef(r/2, 1, 0, 0)
+    bunny.draw()
+    w.flip()
+
+r = 0
+def update(dt):
+    global r
+    r += 90*dt
+    if r > 720: r = 0
+pyglet.clock.schedule(update)
+
+if len(sys.argv) == 1:
+    objfile = os.path.join(os.path.split(__file__)[0], 'rabbit.obj')
+else:
+    objfile = sys.argv[1]
+
+bunny = obj.OBJ(objfile)
+
+pyglet.app.run()
diff --git a/contrib/model/examples/rabbit.mtl b/contrib/model/examples/rabbit.mtl
new file mode 100644
index 0000000..6be13d0
--- /dev/null
+++ b/contrib/model/examples/rabbit.mtl
@@ -0,0 +1,10 @@
+# Exported from Wings 3D 0.98.29b
+newmtl default
+Ns 100.000
+d 1.00000
+illum 2
+Kd 1.00000 1.00000 1.00000
+Ka 1.00000 1.00000 1.00000
+Ks 1.00000 1.00000 1.00000
+Ke 0.00000e+0 0.00000e+0 0.00000e+0
+
diff --git a/contrib/model/examples/rabbit.obj b/contrib/model/examples/rabbit.obj
new file mode 100644
index 0000000..5c91d1d
--- /dev/null
+++ b/contrib/model/examples/rabbit.obj
@@ -0,0 +1,2092 @@
+# Exported from Wings 3D 0.98.29b
+mtllib rabbit.mtl
+o sphere4_cut11
+#65 vertices, 65 faces
+v -0.47730805 4.33748593 0.82378240
+v -0.48231842 4.33748593 0.89231941
+v -0.49658674 4.33748593 0.95042229
+v -0.51794078 4.33748593 0.98924539
+v -0.54312960 4.33748593 1.00287825
+v -0.54312960 4.33748593 0.64468655
+v -0.51794078 4.33748593 0.65831941
+v -0.49658674 4.33748593 0.69714251
+v -0.48231842 4.33748593 0.75524539
+v -0.42150723 4.12071318 0.82378240
+v -0.43076519 4.12071318 0.95042229
+v -0.45712960 4.12071318 1.05778240
+v -0.49658674 4.12071318 1.12951813
+v -0.54312960 4.12071318 1.15470837
+v -0.54312960 4.12071318 0.49285643
+v -0.49658674 4.12071318 0.51804667
+v -0.45712960 4.12071318 0.58978240
+v -0.43076519 4.12071318 0.69714251
+v -0.38422232 3.79628983 0.82378240
+v -0.39631842 3.79628983 0.98924539
+v -0.43076519 3.79628983 1.12951813
+v -0.48231842 3.79628983 1.22324539
+v -0.54312960 3.79628983 1.25615802
+v -0.54312960 3.79628983 0.39140678
+v -0.48231842 3.79628983 0.42431941
+v -0.43076519 3.79628983 0.51804667
+v -0.39631842 3.79628983 0.65831941
+v -0.37112960 3.41360640 0.82378240
+v -0.38422232 3.41360640 1.00287825
+v -0.42150723 3.41360640 1.15470837
+v -0.47730805 3.41360640 1.25615802
+v -0.54312960 3.41360640 1.29178240
+v -0.54312960 3.41360640 0.35578240
+v -0.47730805 3.41360640 0.39140678
+v -0.42150723 3.41360640 0.49285643
+v -0.38422232 3.41360640 0.64468655
+v -0.38422232 3.03092297 0.82378240
+v -0.39631842 3.03092297 0.98924539
+v -0.43076519 3.03092297 1.12951813
+v -0.48231842 3.03092297 1.22324539
+v -0.54312960 3.03092297 1.25615802
+v -0.54312960 3.03092297 0.39140678
+v -0.48231842 3.03092297 0.42431941
+v -0.43076519 3.03092297 0.51804667
+v -0.39631842 3.03092297 0.65831941
+v -0.42150723 2.70649962 0.82378240
+v -0.43076519 2.70649962 0.95042229
+v -0.45712960 2.70649962 1.05778240
+v -0.49658674 2.70649962 1.12951813
+v -0.54312960 2.70649962 1.15470837
+v -0.54312960 2.70649962 0.49285643
+v -0.49658674 2.70649962 0.51804667
+v -0.45712960 2.70649962 0.58978240
+v -0.43076519 2.70649962 0.69714251
+v -0.47730805 2.48972687 0.82378240
+v -0.48231842 2.48972687 0.89231941
+v -0.49658674 2.48972687 0.95042229
+v -0.51794078 2.48972687 0.98924539
+v -0.54312960 2.48972687 1.00287825
+v -0.54312960 2.48972687 0.64468655
+v -0.51794078 2.48972687 0.65831941
+v -0.49658674 2.48972687 0.69714251
+v -0.48231842 2.48972687 0.75524539
+v -0.54312960 4.41360640 0.82378240
+v -0.54312960 2.41360640 0.82378240
+vn 0.88580462 0.46405838 1.4640645e-16
+vn 0.86267949 0.48733515 0.13523516
+vn 0.77196053 0.56262068 0.29586299
+vn 0.52083846 0.69290596 0.49860670
+vn 0.52083846 0.69290596 -0.49860670
+vn 0.77196053 0.56262068 -0.29586299
+vn 0.86267949 0.48733515 -0.13523516
+vn 0.98326758 0.18216714 -1.5689686e-16
+vn 0.96875968 0.19517878 0.15300305
+vn 0.90425783 0.24266597 0.35132749
+vn 0.66538177 0.34948186 0.65964349
+vn 0.66538177 0.34948186 -0.65964349
+vn 0.90425783 0.24266597 -0.35132749
+vn 0.96875968 0.19517878 -0.15300305
+vn 0.99724008 7.4244402e-2 -3.7947699e-16
+vn 0.98457777 7.9770475e-2 0.15570256
+vn 0.92704382 0.10040034 0.36126103
+vn 0.69787980 0.15014160 0.70030086
+vn 0.69787980 0.15014160 -0.70030086
+vn 0.92704382 0.10040034 -0.36126103
+vn 0.98457777 7.9770475e-2 -0.15570256
+vn 1.00000000 0.0000000e+0 -3.1326570e-16
+vn 0.98771924 1.7420240e-17 0.15623925
+vn 0.93167648 -1.0509712e-17 0.36328906
+vn 0.70490553 -1.0149541e-16 0.70930120
+vn 0.70490553 1.4499344e-17 -0.70930120
+vn 0.93167648 3.5032373e-18 -0.36328906
+vn 0.98771924 8.7101199e-18 -0.15623925
+vn 0.99724008 -7.4244402e-2 -3.6903267e-16
+vn 0.98457777 -7.9770475e-2 0.15570256
+vn 0.92704382 -0.10040034 0.36126103
+vn 0.69787980 -0.15014160 0.70030086
+vn 0.69787980 -0.15014160 -0.70030086
+vn 0.92704382 -0.10040034 -0.36126103
+vn 0.98457777 -7.9770475e-2 -0.15570256
+vn 0.98326758 -0.18216714 -1.3249069e-16
+vn 0.96875968 -0.19517878 0.15300305
+vn 0.90425783 -0.24266597 0.35132749
+vn 0.66538177 -0.34948186 0.65964349
+vn 0.66538177 -0.34948186 -0.65964349
+vn 0.90425783 -0.24266597 -0.35132749
+vn 0.96875968 -0.19517878 -0.15300305
+vn 0.88580462 -0.46405838 1.5711911e-16
+vn 0.86267949 -0.48733515 0.13523516
+vn 0.77196053 -0.56262068 0.29586299
+vn 0.52083846 -0.69290596 0.49860670
+vn 0.52083846 -0.69290596 -0.49860670
+vn 0.77196053 -0.56262068 -0.29586299
+vn 0.86267949 -0.48733515 -0.13523516
+vn 0.31899373 0.74220086 0.58939026
+vn 0.31899373 0.74220086 0.58939026
+vn -1.00000000 0.0000000e+0 1.2638195e-16
+vn 0.31899373 0.74220086 -0.58939026
+vn 0.31899373 0.74220086 -0.58939026
+vn -1.00000000 0.0000000e+0 1.2638195e-16
+vn 0.43649078 0.39882199 0.80648423
+vn 0.43649078 0.39882199 0.80648423
+vn -1.00000000 0.0000000e+0 1.2638195e-16
+vn 0.43649078 0.39882199 -0.80648423
+vn 0.43649078 0.39882199 -0.80648423
+vn -1.00000000 0.0000000e+0 1.2638195e-16
+vn 0.46871136 0.17413983 0.86601673
+vn 0.46871136 0.17413983 0.86601673
+vn -1.00000000 0.0000000e+0 1.2638195e-16
+vn 0.46871136 0.17413983 -0.86601673
+vn 0.46871136 0.17413983 -0.86601673
+vn -1.00000000 0.0000000e+0 1.2638195e-16
+vn 0.47598395 -1.3228008e-16 0.87945397
+vn 0.47598395 -1.3228008e-16 0.87945397
+vn -1.00000000 0.0000000e+0 1.2638195e-16
+vn 0.47598395 4.1772656e-17 -0.87945397
+vn 0.47598395 4.1772656e-17 -0.87945397
+vn -1.00000000 0.0000000e+0 1.2638195e-16
+vn 0.46871136 -0.17413983 0.86601673
+vn 0.46871136 -0.17413983 0.86601673
+vn -1.00000000 0.0000000e+0 1.2638195e-16
+vn 0.46871136 -0.17413983 -0.86601673
+vn 0.46871136 -0.17413983 -0.86601673
+vn -1.00000000 0.0000000e+0 1.2638195e-16
+vn 0.43649078 -0.39882199 0.80648423
+vn 0.43649078 -0.39882199 0.80648423
+vn -1.00000000 0.0000000e+0 1.2638195e-16
+vn 0.43649078 -0.39882199 -0.80648423
+vn 0.43649078 -0.39882199 -0.80648423
+vn -1.00000000 0.0000000e+0 1.2638195e-16
+vn 0.31899373 -0.74220086 0.58939026
+vn 0.31899373 -0.74220086 0.58939026
+vn -1.00000000 0.0000000e+0 1.2638195e-16
+vn 0.31899373 -0.74220086 -0.58939026
+vn 0.31899373 -0.74220086 -0.58939026
+vn -1.00000000 0.0000000e+0 1.2638195e-16
+vn 0.57994027 0.81465900 -1.0813601e-16
+vn 0.57994027 0.81465900 -1.0721177e-16
+vn 0.57994027 0.81465900 -1.1090873e-16
+vn 0.57994027 0.81465900 -1.1090873e-16
+vn 0.57994027 0.81465900 -1.0351481e-16
+vn 0.57994027 0.81465900 -1.0351481e-16
+vn 0.57994027 0.81465900 -9.9817855e-17
+vn 0.57994027 0.81465900 -1.0351481e-16
+vn -1.00000000 0.0000000e+0 1.2638195e-16
+vn 0.57994027 -0.81465900 -1.8207516e-16
+vn 0.57994027 -0.81465900 -1.7745396e-16
+vn 0.57994027 -0.81465900 -1.7745396e-16
+vn 0.57994027 -0.81465900 -1.7745396e-16
+vn 0.57994027 -0.81465900 -1.9224179e-16
+vn 0.57994027 -0.81465900 -1.9224179e-16
+vn 0.57994027 -0.81465900 -1.8854484e-16
+vn 0.57994027 -0.81465900 -1.9224179e-16
+vn -1.00000000 0.0000000e+0 1.2638195e-16
+g sphere4_cut11_default
+usemtl default
+f 1//1 10//8 18//14 9//7
+f 1//1 64//92 2//2
+f 2//2 11//9 10//8 1//1
+f 2//2 64//93 3//3
+f 3//3 12//10 11//9 2//2
+f 3//3 64//94 4//4
+f 4//4 13//11 12//10 3//3
+f 4//4 64//95 5//50
+f 5//51 14//56 13//11 4//4
+f 5//52 64//100 6//55 15//61 24//67 33//73 42//79 51//85 60//91 65//109 59//88 50//82 41//76 32//70 23//64 14//58
+f 6//53 64//96 7//5
+f 7//5 16//12 15//59 6//54
+f 7//5 64//97 8//6
+f 8//6 17//13 16//12 7//5
+f 8//6 64//98 9//7
+f 9//7 18//14 17//13 8//6
+f 9//7 64//99 1//1
+f 10//8 19//15 27//21 18//14
+f 11//9 20//16 19//15 10//8
+f 12//10 21//17 20//16 11//9
+f 13//11 22//18 21//17 12//10
+f 14//57 23//62 22//18 13//11
+f 16//12 25//19 24//65 15//60
+f 17//13 26//20 25//19 16//12
+f 18//14 27//21 26//20 17//13
+f 19//15 28//22 36//28 27//21
+f 20//16 29//23 28//22 19//15
+f 21//17 30//24 29//23 20//16
+f 22//18 31//25 30//24 21//17
+f 23//63 32//68 31//25 22//18
+f 25//19 34//26 33//71 24//66
+f 26//20 35//27 34//26 25//19
+f 27//21 36//28 35//27 26//20
+f 28//22 37//29 45//35 36//28
+f 29//23 38//30 37//29 28//22
+f 30//24 39//31 38//30 29//23
+f 31//25 40//32 39//31 30//24
+f 32//69 41//74 40//32 31//25
+f 34//26 43//33 42//77 33//72
+f 35//27 44//34 43//33 34//26
+f 36//28 45//35 44//34 35//27
+f 37//29 46//36 54//42 45//35
+f 38//30 47//37 46//36 37//29
+f 39//31 48//38 47//37 38//30
+f 40//32 49//39 48//38 39//31
+f 41//75 50//80 49//39 40//32
+f 43//33 52//40 51//83 42//78
+f 44//34 53//41 52//40 43//33
+f 45//35 54//42 53//41 44//34
+f 46//36 55//43 63//49 54//42
+f 47//37 56//44 55//43 46//36
+f 48//38 57//45 56//44 47//37
+f 49//39 58//46 57//45 48//38
+f 50//81 59//87 58//46 49//39
+f 52//40 61//47 60//90 51//84
+f 53//41 62//48 61//47 52//40
+f 54//42 63//49 62//48 53//41
+f 55//43 65//108 63//49
+f 56//44 65//101 55//43
+f 57//45 65//102 56//44
+f 58//46 65//103 57//45
+f 59//86 65//104 58//46
+f 61//47 65//105 60//89
+f 62//48 65//106 61//47
+f 63//49 65//107 62//48
+o sphere6_copy11
+#65 vertices, 65 faces
+v -0.65273225 0.54878444 0.66895360
+v -0.66010344 0.54878444 0.72584459
+v -0.68109479 0.54878444 0.77407445
+v -0.71251056 0.54878444 0.80630061
+v -0.74956800 0.54878444 0.81761693
+v -0.78662544 0.54878444 0.80630061
+v -0.81804121 0.54878444 0.77407445
+v -0.83903256 0.54878444 0.72584459
+v -0.84640375 0.54878444 0.66895360
+v -0.83903256 0.54878444 0.61206261
+v -0.81804121 0.54878444 0.56383275
+v -0.78662544 0.54878444 0.53160659
+v -0.74956800 0.54878444 0.52029027
+v -0.71251056 0.54878444 0.53160659
+v -0.68109479 0.54878444 0.56383275
+v -0.66010344 0.54878444 0.61206261
+v -0.57063887 0.42002143 0.66895360
+v -0.58425904 0.42002143 0.77407445
+v -0.62304600 0.42002143 0.86319160
+v -0.68109479 0.42002143 0.92273778
+v -0.74956800 0.42002143 0.94364761
+v -0.81804121 0.42002143 0.92273778
+v -0.87609000 0.42002143 0.86319160
+v -0.91487696 0.42002143 0.77407445
+v -0.92849713 0.42002143 0.66895360
+v -0.91487696 0.42002143 0.56383275
+v -0.87609000 0.42002143 0.47471560
+v -0.81804121 0.42002143 0.41516942
+v -0.74956800 0.42002143 0.39425959
+v -0.68109479 0.42002143 0.41516942
+v -0.62304600 0.42002143 0.47471560
+v -0.58425904 0.42002143 0.56383275
+v -0.51578583 0.22731396 0.66895360
+v -0.53358144 0.22731396 0.80630061
+v -0.58425904 0.22731396 0.92273778
+v -0.66010344 0.22731396 1.00053861
+v -0.74956800 0.22731396 1.02785863
+v -0.83903256 0.22731396 1.00053861
+v -0.91487696 0.22731396 0.92273778
+v -0.96555456 0.22731396 0.80630061
+v -0.98335017 0.22731396 0.66895360
+v -0.96555456 0.22731396 0.53160659
+v -0.91487696 0.22731396 0.41516942
+v -0.83903256 0.22731396 0.33736859
+v -0.74956800 0.22731396 0.31004857
+v -0.66010344 0.22731396 0.33736859
+v -0.58425904 0.22731396 0.41516942
+v -0.53358144 0.22731396 0.53160659
+v -0.49652400 5.7206944e-17 0.66895360
+v -0.51578583 5.7206944e-17 0.81761693
+v -0.57063887 5.7206944e-17 0.94364761
+v -0.65273225 5.7206944e-17 1.02785863
+v -0.74956800 5.7206944e-17 1.05742960
+v -0.84640375 5.7206944e-17 1.02785863
+v -0.92849713 5.7206944e-17 0.94364761
+v -0.98335017 5.7206944e-17 0.81761693
+v -1.00261200 5.7206944e-17 0.66895360
+v -0.98335017 5.7206944e-17 0.52029027
+v -0.92849713 5.7206944e-17 0.39425959
+v -0.84640375 5.7206944e-17 0.31004857
+v -0.74956800 5.7206944e-17 0.28047760
+v -0.65273225 5.7206944e-17 0.31004857
+v -0.57063887 5.7206944e-17 0.39425959
+v -0.51578583 5.7206944e-17 0.52029027
+v -0.74956800 0.59400000 0.66895360
+vn 0.65892374 0.75220974 -1.8496749e-16
+vn 0.62180762 0.76451654 0.16991097
+vn 0.50298875 0.79774391 0.33257626
+vn 0.28957537 0.83730436 0.46375372
+vn 8.7120189e-17 0.85605025 0.51689261
+vn -0.28957537 0.83730436 0.46375372
+vn -0.50298875 0.79774391 0.33257626
+vn -0.62180762 0.76451654 0.16991097
+vn -0.65892374 0.75220974 -1.7408705e-16
+vn -0.62180762 0.76451654 -0.16991097
+vn -0.50298875 0.79774391 -0.33257626
+vn -0.28957537 0.83730436 -0.46375372
+vn 1.2342027e-16 0.85605025 -0.51689261
+vn 0.28957537 0.83730436 -0.46375372
+vn 0.50298875 0.79774391 -0.33257626
+vn 0.62180762 0.76451654 -0.16991097
+vn 0.91203694 0.41010806 -3.3184811e-16
+vn 0.87268147 0.42514908 0.24015683
+vn 0.73458410 0.46911028 0.49022623
+vn 0.44532987 0.52876101 0.72256010
+vn 5.8181617e-17 0.56021969 0.82834407
+vn -0.44532987 0.52876101 0.72256010
+vn -0.73458410 0.46911028 0.49022623
+vn -0.87268147 0.42514908 0.24015683
+vn -0.91203694 0.41010806 -3.8480260e-16
+vn -0.87268147 0.42514908 -0.24015683
+vn -0.73458410 0.46911028 -0.49022623
+vn -0.44532987 0.52876101 -0.72256010
+vn -7.2727021e-17 0.56021969 -0.82834407
+vn 0.44532987 0.52876101 -0.72256010
+vn 0.73458410 0.46911028 -0.49022623
+vn 0.87268147 0.42514908 -0.24015683
+vn 0.98367327 0.17996358 -3.6541134e-16
+vn 0.94674796 0.18795399 0.26142224
+vn 0.81155517 0.21229106 0.54434429
+vn 0.50522471 0.24784135 0.82663333
+vn -1.4600896e-16 0.26780203 0.96347396
+vn -0.50522471 0.24784135 0.82663333
+vn -0.81155517 0.21229106 0.54434429
+vn -0.94674796 0.18795399 0.26142224
+vn -0.98367327 0.17996358 -4.2865561e-16
+vn -0.94674796 0.18795399 -0.26142224
+vn -0.81155517 0.21229106 -0.54434429
+vn -0.50522471 0.24784135 -0.82663333
+vn -2.4821524e-16 0.26780203 -0.96347396
+vn 0.50522471 0.24784135 -0.82663333
+vn 0.81155517 0.21229106 -0.54434429
+vn 0.94674796 0.18795399 -0.26142224
+vn 0.92158026 -0.38818787 -3.7531383e-16
+vn 0.88923136 -0.38584305 0.24574933
+vn 0.76813703 -0.37919595 0.51592241
+vn 0.48330787 -0.37149107 0.79272182
+vn -1.6310066e-16 -0.36877343 0.92951932
+vn -0.48330787 -0.37149107 0.79272182
+vn -0.76813703 -0.37919595 0.51592241
+vn -0.88923136 -0.38584305 0.24574933
+vn -0.92158026 -0.38818787 -3.7531383e-16
+vn -0.88923136 -0.38584305 -0.24574933
+vn -0.76813703 -0.37919595 -0.51592241
+vn -0.48330787 -0.37149107 -0.79272182
+vn -2.4465099e-16 -0.36877343 -0.92951932
+vn 0.48330787 -0.37149107 -0.79272182
+vn 0.76813703 -0.37919595 -0.51592241
+vn 0.88923136 -0.38584305 -0.24574933
+vn 9.3419028e-17 1.00000000 -6.3478229e-16
+g sphere6_copy11_default
+usemtl default
+f 66//110 82//126 97//141 81//125
+f 66//110 130//174 67//111
+f 67//111 83//127 82//126 66//110
+f 67//111 130//174 68//112
+f 68//112 84//128 83//127 67//111
+f 68//112 130//174 69//113
+f 69//113 85//129 84//128 68//112
+f 69//113 130//174 70//114
+f 70//114 86//130 85//129 69//113
+f 70//114 130//174 71//115
+f 71//115 87//131 86//130 70//114
+f 71//115 130//174 72//116
+f 72//116 88//132 87//131 71//115
+f 72//116 130//174 73//117
+f 73//117 89//133 88//132 72//116
+f 73//117 130//174 74//118
+f 74//118 90//134 89//133 73//117
+f 74//118 130//174 75//119
+f 75//119 91//135 90//134 74//118
+f 75//119 130//174 76//120
+f 76//120 92//136 91//135 75//119
+f 76//120 130//174 77//121
+f 77//121 93//137 92//136 76//120
+f 77//121 130//174 78//122
+f 78//122 94//138 93//137 77//121
+f 78//122 130//174 79//123
+f 79//123 95//139 94//138 78//122
+f 79//123 130//174 80//124
+f 80//124 96//140 95//139 79//123
+f 80//124 130//174 81//125
+f 81//125 97//141 96//140 80//124
+f 81//125 130//174 66//110
+f 82//126 98//142 113//157 97//141
+f 83//127 99//143 98//142 82//126
+f 84//128 100//144 99//143 83//127
+f 85//129 101//145 100//144 84//128
+f 86//130 102//146 101//145 85//129
+f 87//131 103//147 102//146 86//130
+f 88//132 104//148 103//147 87//131
+f 89//133 105//149 104//148 88//132
+f 90//134 106//150 105//149 89//133
+f 91//135 107//151 106//150 90//134
+f 92//136 108//152 107//151 91//135
+f 93//137 109//153 108//152 92//136
+f 94//138 110//154 109//153 93//137
+f 95//139 111//155 110//154 94//138
+f 96//140 112//156 111//155 95//139
+f 97//141 113//157 112//156 96//140
+f 98//142 114//158 129//173 113//157
+f 99//143 115//159 114//158 98//142
+f 100//144 116//160 115//159 99//143
+f 101//145 117//161 116//160 100//144
+f 102//146 118//162 117//161 101//145
+f 103//147 119//163 118//162 102//146
+f 104//148 120//164 119//163 103//147
+f 105//149 121//165 120//164 104//148
+f 106//150 122//166 121//165 105//149
+f 107//151 123//167 122//166 106//150
+f 108//152 124//168 123//167 107//151
+f 109//153 125//169 124//168 108//152
+f 110//154 126//170 125//169 109//153
+f 111//155 127//171 126//170 110//154
+f 112//156 128//172 127//171 111//155
+f 113//157 129//173 128//172 112//156
+f 115//159 116//160 117//161 118//162 119//163 120//164 121//165 122//166 123//167 124//168 125//169 126//170 127//171 128//172 129//173 114//158
+o sphere6_copy10
+#65 vertices, 65 faces
+v 0.82182775 0.54878444 0.66895360
+v 0.81445656 0.54878444 0.72584459
+v 0.79346521 0.54878444 0.77407445
+v 0.76204944 0.54878444 0.80630061
+v 0.72499200 0.54878444 0.81761693
+v 0.68793456 0.54878444 0.80630061
+v 0.65651879 0.54878444 0.77407445
+v 0.63552744 0.54878444 0.72584459
+v 0.62815625 0.54878444 0.66895360
+v 0.63552744 0.54878444 0.61206261
+v 0.65651879 0.54878444 0.56383275
+v 0.68793456 0.54878444 0.53160659
+v 0.72499200 0.54878444 0.52029027
+v 0.76204944 0.54878444 0.53160659
+v 0.79346521 0.54878444 0.56383275
+v 0.81445656 0.54878444 0.61206261
+v 0.90392113 0.42002143 0.66895360
+v 0.89030096 0.42002143 0.77407445
+v 0.85151400 0.42002143 0.86319160
+v 0.79346521 0.42002143 0.92273778
+v 0.72499200 0.42002143 0.94364761
+v 0.65651879 0.42002143 0.92273778
+v 0.59847000 0.42002143 0.86319160
+v 0.55968304 0.42002143 0.77407445
+v 0.54606287 0.42002143 0.66895360
+v 0.55968304 0.42002143 0.56383275
+v 0.59847000 0.42002143 0.47471560
+v 0.65651879 0.42002143 0.41516942
+v 0.72499200 0.42002143 0.39425959
+v 0.79346521 0.42002143 0.41516942
+v 0.85151400 0.42002143 0.47471560
+v 0.89030096 0.42002143 0.56383275
+v 0.95877417 0.22731396 0.66895360
+v 0.94097856 0.22731396 0.80630061
+v 0.89030096 0.22731396 0.92273778
+v 0.81445656 0.22731396 1.00053861
+v 0.72499200 0.22731396 1.02785863
+v 0.63552744 0.22731396 1.00053861
+v 0.55968304 0.22731396 0.92273778
+v 0.50900544 0.22731396 0.80630061
+v 0.49120983 0.22731396 0.66895360
+v 0.50900544 0.22731396 0.53160659
+v 0.55968304 0.22731396 0.41516942
+v 0.63552744 0.22731396 0.33736859
+v 0.72499200 0.22731396 0.31004857
+v 0.81445656 0.22731396 0.33736859
+v 0.89030096 0.22731396 0.41516942
+v 0.94097856 0.22731396 0.53160659
+v 0.97803600 5.7206944e-17 0.66895360
+v 0.95877417 5.7206944e-17 0.81761693
+v 0.90392113 5.7206944e-17 0.94364761
+v 0.82182775 5.7206944e-17 1.02785863
+v 0.72499200 5.7206944e-17 1.05742960
+v 0.62815625 5.7206944e-17 1.02785863
+v 0.54606287 5.7206944e-17 0.94364761
+v 0.49120983 5.7206944e-17 0.81761693
+v 0.47194800 5.7206944e-17 0.66895360
+v 0.49120983 5.7206944e-17 0.52029027
+v 0.54606287 5.7206944e-17 0.39425959
+v 0.62815625 5.7206944e-17 0.31004857
+v 0.72499200 5.7206944e-17 0.28047760
+v 0.82182775 5.7206944e-17 0.31004857
+v 0.90392113 5.7206944e-17 0.39425959
+v 0.95877417 5.7206944e-17 0.52029027
+v 0.72499200 0.59400000 0.66895360
+vn 0.65892374 0.75220974 -1.8496749e-16
+vn 0.62180762 0.76451654 0.16991097
+vn 0.50298875 0.79774391 0.33257626
+vn 0.28957537 0.83730436 0.46375372
+vn 1.1616025e-16 0.85605025 0.51689261
+vn -0.28957537 0.83730436 0.46375372
+vn -0.50298875 0.79774391 0.33257626
+vn -0.62180762 0.76451654 0.16991097
+vn -0.65892374 0.75220974 -1.7408705e-16
+vn -0.62180762 0.76451654 -0.16991097
+vn -0.50298875 0.79774391 -0.33257626
+vn -0.28957537 0.83730436 -0.46375372
+vn 1.3794030e-16 0.85605025 -0.51689261
+vn 0.28957537 0.83730436 -0.46375372
+vn 0.50298875 0.79774391 -0.33257626
+vn 0.62180762 0.76451654 -0.16991097
+vn 0.91203694 0.41010806 -3.2831781e-16
+vn 0.87268147 0.42514908 0.24015683
+vn 0.73458410 0.46911028 0.49022623
+vn 0.44532987 0.52876101 0.72256010
+vn -4.3636213e-17 0.56021969 0.82834407
+vn -0.44532987 0.52876101 0.72256010
+vn -0.73458410 0.46911028 0.49022623
+vn -0.87268147 0.42514908 0.24015683
+vn -0.91203694 0.41010806 -3.8127230e-16
+vn -0.87268147 0.42514908 -0.24015683
+vn -0.73458410 0.46911028 -0.49022623
+vn -0.44532987 0.52876101 -0.72256010
+vn 5.8181617e-17 0.56021969 -0.82834407
+vn 0.44532987 0.52876101 -0.72256010
+vn 0.73458410 0.46911028 -0.49022623
+vn 0.87268147 0.42514908 -0.24015683
+vn 0.98367327 0.17996358 -4.1460133e-16
+vn 0.94674796 0.18795399 0.26142224
+vn 0.81155517 0.21229106 0.54434429
+vn 0.50522471 0.24784135 0.82663333
+vn -2.1901345e-16 0.26780203 0.96347396
+vn -0.50522471 0.24784135 0.82663333
+vn -0.81155517 0.21229106 0.54434429
+vn -0.94674796 0.18795399 0.26142224
+vn -0.98367327 0.17996358 -4.2865561e-16
+vn -0.94674796 0.18795399 -0.26142224
+vn -0.81155517 0.21229106 -0.54434429
+vn -0.50522471 0.24784135 -0.82663333
+vn -1.0220628e-16 0.26780203 -0.96347396
+vn 0.50522471 0.24784135 -0.82663333
+vn 0.81155517 0.21229106 -0.54434429
+vn 0.94674796 0.18795399 -0.26142224
+vn 0.92158026 -0.38818787 -4.7884868e-16
+vn 0.88923136 -0.38584305 0.24574933
+vn 0.76813703 -0.37919595 0.51592241
+vn 0.48330787 -0.37149107 0.79272182
+vn -1.6310066e-16 -0.36877343 0.92951932
+vn -0.48330787 -0.37149107 0.79272182
+vn -0.76813703 -0.37919595 0.51592241
+vn -0.88923136 -0.38584305 0.24574933
+vn -0.92158026 -0.38818787 -3.8825568e-16
+vn -0.88923136 -0.38584305 -0.24574933
+vn -0.76813703 -0.37919595 -0.51592241
+vn -0.48330787 -0.37149107 -0.79272182
+vn -2.4465099e-16 -0.36877343 -0.92951932
+vn 0.48330787 -0.37149107 -0.79272182
+vn 0.76813703 -0.37919595 -0.51592241
+vn 0.88923136 -0.38584305 -0.24574933
+vn 3.7367611e-18 1.00000000 -6.3104553e-16
+g sphere6_copy10_default
+usemtl default
+f 131//175 147//191 162//206 146//190
+f 131//175 195//239 132//176
+f 132//176 148//192 147//191 131//175
+f 132//176 195//239 133//177
+f 133//177 149//193 148//192 132//176
+f 133//177 195//239 134//178
+f 134//178 150//194 149//193 133//177
+f 134//178 195//239 135//179
+f 135//179 151//195 150//194 134//178
+f 135//179 195//239 136//180
+f 136//180 152//196 151//195 135//179
+f 136//180 195//239 137//181
+f 137//181 153//197 152//196 136//180
+f 137//181 195//239 138//182
+f 138//182 154//198 153//197 137//181
+f 138//182 195//239 139//183
+f 139//183 155//199 154//198 138//182
+f 139//183 195//239 140//184
+f 140//184 156//200 155//199 139//183
+f 140//184 195//239 141//185
+f 141//185 157//201 156//200 140//184
+f 141//185 195//239 142//186
+f 142//186 158//202 157//201 141//185
+f 142//186 195//239 143//187
+f 143//187 159//203 158//202 142//186
+f 143//187 195//239 144//188
+f 144//188 160//204 159//203 143//187
+f 144//188 195//239 145//189
+f 145//189 161//205 160//204 144//188
+f 145//189 195//239 146//190
+f 146//190 162//206 161//205 145//189
+f 146//190 195//239 131//175
+f 147//191 163//207 178//222 162//206
+f 148//192 164//208 163//207 147//191
+f 149//193 165//209 164//208 148//192
+f 150//194 166//210 165//209 149//193
+f 151//195 167//211 166//210 150//194
+f 152//196 168//212 167//211 151//195
+f 153//197 169//213 168//212 152//196
+f 154//198 170//214 169//213 153//197
+f 155//199 171//215 170//214 154//198
+f 156//200 172//216 171//215 155//199
+f 157//201 173//217 172//216 156//200
+f 158//202 174//218 173//217 157//201
+f 159//203 175//219 174//218 158//202
+f 160//204 176//220 175//219 159//203
+f 161//205 177//221 176//220 160//204
+f 162//206 178//222 177//221 161//205
+f 163//207 179//223 194//238 178//222
+f 164//208 180//224 179//223 163//207
+f 165//209 181//225 180//224 164//208
+f 166//210 182//226 181//225 165//209
+f 167//211 183//227 182//226 166//210
+f 168//212 184//228 183//227 167//211
+f 169//213 185//229 184//228 168//212
+f 170//214 186//230 185//229 169//213
+f 171//215 187//231 186//230 170//214
+f 172//216 188//232 187//231 171//215
+f 173//217 189//233 188//232 172//216
+f 174//218 190//234 189//233 173//217
+f 175//219 191//235 190//234 174//218
+f 176//220 192//236 191//235 175//219
+f 177//221 193//237 192//236 176//220
+f 178//222 194//238 193//237 177//221
+f 180//224 181//225 182//226 183//227 184//228 185//229 186//230 187//231 188//232 189//233 190//234 191//235 192//236 193//237 194//238 179//223
+o sphere6_copy8
+#65 vertices, 65 faces
+v -0.65273225 0.54878444 -0.55984640
+v -0.66010344 0.54878444 -0.47285711
+v -0.68109479 0.54878444 -0.39911116
+v -0.71251056 0.54878444 -0.34983569
+v -0.74956800 0.54878444 -0.33253244
+v -0.78662544 0.54878444 -0.34983569
+v -0.81804121 0.54878444 -0.39911116
+v -0.83903256 0.54878444 -0.47285711
+v -0.84640375 0.54878444 -0.55984640
+v -0.83903256 0.54878444 -0.64683569
+v -0.81804121 0.54878444 -0.72058164
+v -0.78662544 0.54878444 -0.76985711
+v -0.74956800 0.54878444 -0.78716036
+v -0.71251056 0.54878444 -0.76985711
+v -0.68109479 0.54878444 -0.72058164
+v -0.66010344 0.54878444 -0.64683569
+v -0.57063887 0.42002143 -0.55984640
+v -0.58425904 0.42002143 -0.39911116
+v -0.62304600 0.42002143 -0.26284640
+v -0.68109479 0.42002143 -0.17179720
+v -0.74956800 0.42002143 -0.13982497
+v -0.81804121 0.42002143 -0.17179720
+v -0.87609000 0.42002143 -0.26284640
+v -0.91487696 0.42002143 -0.39911116
+v -0.92849713 0.42002143 -0.55984640
+v -0.91487696 0.42002143 -0.72058164
+v -0.87609000 0.42002143 -0.85684640
+v -0.81804121 0.42002143 -0.94789560
+v -0.74956800 0.42002143 -0.97986783
+v -0.68109479 0.42002143 -0.94789560
+v -0.62304600 0.42002143 -0.85684640
+v -0.58425904 0.42002143 -0.72058164
+v -0.51578583 0.22731396 -0.55984640
+v -0.53358144 0.22731396 -0.34983569
+v -0.58425904 0.22731396 -0.17179720
+v -0.66010344 0.22731396 -5.2835686e-2
+v -0.74956800 0.22731396 -1.1061958e-2
+v -0.83903256 0.22731396 -5.2835686e-2
+v -0.91487696 0.22731396 -0.17179720
+v -0.96555456 0.22731396 -0.34983569
+v -0.98335017 0.22731396 -0.55984640
+v -0.96555456 0.22731396 -0.76985711
+v -0.91487696 0.22731396 -0.94789560
+v -0.83903256 0.22731396 -1.06685711
+v -0.74956800 0.22731396 -1.10863084
+v -0.66010344 0.22731396 -1.06685711
+v -0.58425904 0.22731396 -0.94789560
+v -0.53358144 0.22731396 -0.76985711
+v -0.49652400 5.7206944e-17 -0.55984640
+v -0.51578583 5.7206944e-17 -0.33253244
+v -0.57063887 5.7206944e-17 -0.13982497
+v -0.65273225 5.7206944e-17 -1.1061958e-2
+v -0.74956800 5.7206944e-17 3.4153600e-2
+v -0.84640375 5.7206944e-17 -1.1061958e-2
+v -0.92849713 5.7206944e-17 -0.13982497
+v -0.98335017 5.7206944e-17 -0.33253244
+v -1.00261200 5.7206944e-17 -0.55984640
+v -0.98335017 5.7206944e-17 -0.78716036
+v -0.92849713 5.7206944e-17 -0.97986783
+v -0.84640375 5.7206944e-17 -1.10863084
+v -0.74956800 5.7206944e-17 -1.15384640
+v -0.65273225 5.7206944e-17 -1.10863084
+v -0.57063887 5.7206944e-17 -0.97986783
+v -0.51578583 5.7206944e-17 -0.78716036
+v -0.74956800 0.59400000 -0.55984640
+vn 0.65920045 0.75196727 5.4286987e-17
+vn 0.62817281 0.76983505 0.11292879
+vn 0.52318843 0.82106426 0.22831414
+vn 0.31317680 0.88932505 0.33319850
+vn 8.6461005e-17 0.92495788 0.38006962
+vn -0.31317680 0.88932505 0.33319850
+vn -0.52318843 0.82106426 0.22831414
+vn -0.62817281 0.76983505 0.11292879
+vn -0.65920045 0.75196727 0.0000000e+0
+vn -0.62817281 0.76983505 -0.11292879
+vn -0.52318843 0.82106426 -0.22831414
+vn -0.31317680 0.88932505 -0.33319850
+vn 2.5938301e-16 0.92495788 -0.38006962
+vn 0.31317680 0.88932505 -0.33319850
+vn 0.52318843 0.82106426 -0.22831414
+vn 0.62817281 0.76983505 -0.11292879
+vn 0.91206716 0.41004084 5.2748873e-17
+vn 0.88721918 0.43227186 0.16119295
+vn 0.78910428 0.50478589 0.35000806
+vn 0.52368375 0.62842774 0.57518164
+vn 1.4892951e-17 0.70974817 0.70445549
+vn -0.52368375 0.62842774 0.57518164
+vn -0.78910428 0.50478589 0.35000806
+vn -0.88721918 0.43227186 0.16119295
+vn -0.91206716 0.41004084 -7.0331830e-18
+vn -0.88721918 0.43227186 -0.16119295
+vn -0.78910428 0.50478589 -0.35000806
+vn -0.52368375 0.62842774 -0.57518164
+vn -1.6382246e-16 0.70974817 -0.70445549
+vn 0.52368375 0.62842774 -0.57518164
+vn 0.78910428 0.50478589 -0.35000806
+vn 0.88721918 0.43227186 -0.16119295
+vn 0.98367604 0.17994844 -3.8473664e-17
+vn 0.96545175 0.19187092 0.17631923
+vn 0.88733269 0.23376762 0.39748383
+vn 0.62911347 0.31848577 0.70907197
+vn -1.2294647e-16 0.38497918 0.92292526
+vn -0.62911347 0.31848577 0.70907197
+vn -0.88733269 0.23376762 0.39748383
+vn -0.96545175 0.19187092 0.17631923
+vn -0.98367604 0.17994844 -1.7488029e-17
+vn -0.96545175 0.19187092 -0.17631923
+vn -0.88733269 0.23376762 -0.39748383
+vn -0.62911347 0.31848577 -0.70907197
+vn -3.8420771e-16 0.38497918 -0.92292526
+vn 0.62911347 0.31848577 -0.70907197
+vn 0.88733269 0.23376762 -0.39748383
+vn 0.96545175 0.19187092 -0.17631923
+vn 0.92236342 -0.38632333 -1.2891947e-16
+vn 0.90897968 -0.38226311 0.16622532
+vn 0.84805657 -0.36833221 0.38096120
+vn 0.61975352 -0.34499831 0.70489839
+vn -2.3415204e-16 -0.34021686 0.94034700
+vn -0.61975352 -0.34499831 0.70489839
+vn -0.84805657 -0.36833221 0.38096120
+vn -0.90897968 -0.38226311 0.16622532
+vn -0.92236342 -0.38632333 -1.9337920e-17
+vn -0.90897968 -0.38226311 -0.16622532
+vn -0.84805657 -0.36833221 -0.38096120
+vn -0.61975352 -0.34499831 -0.70489839
+vn -3.5122805e-16 -0.34021686 -0.94034700
+vn 0.61975352 -0.34499831 -0.70489839
+vn 0.84805657 -0.36833221 -0.38096120
+vn 0.90897968 -0.38226311 -0.16622532
+vn 1.1069310e-16 1.00000000 -6.9183187e-17
+g sphere6_copy8_default
+usemtl default
+f 196//240 212//256 227//271 211//255
+f 196//240 260//304 197//241
+f 197//241 213//257 212//256 196//240
+f 197//241 260//304 198//242
+f 198//242 214//258 213//257 197//241
+f 198//242 260//304 199//243
+f 199//243 215//259 214//258 198//242
+f 199//243 260//304 200//244
+f 200//244 216//260 215//259 199//243
+f 200//244 260//304 201//245
+f 201//245 217//261 216//260 200//244
+f 201//245 260//304 202//246
+f 202//246 218//262 217//261 201//245
+f 202//246 260//304 203//247
+f 203//247 219//263 218//262 202//246
+f 203//247 260//304 204//248
+f 204//248 220//264 219//263 203//247
+f 204//248 260//304 205//249
+f 205//249 221//265 220//264 204//248
+f 205//249 260//304 206//250
+f 206//250 222//266 221//265 205//249
+f 206//250 260//304 207//251
+f 207//251 223//267 222//266 206//250
+f 207//251 260//304 208//252
+f 208//252 224//268 223//267 207//251
+f 208//252 260//304 209//253
+f 209//253 225//269 224//268 208//252
+f 209//253 260//304 210//254
+f 210//254 226//270 225//269 209//253
+f 210//254 260//304 211//255
+f 211//255 227//271 226//270 210//254
+f 211//255 260//304 196//240
+f 212//256 228//272 243//287 227//271
+f 213//257 229//273 228//272 212//256
+f 214//258 230//274 229//273 213//257
+f 215//259 231//275 230//274 214//258
+f 216//260 232//276 231//275 215//259
+f 217//261 233//277 232//276 216//260
+f 218//262 234//278 233//277 217//261
+f 219//263 235//279 234//278 218//262
+f 220//264 236//280 235//279 219//263
+f 221//265 237//281 236//280 220//264
+f 222//266 238//282 237//281 221//265
+f 223//267 239//283 238//282 222//266
+f 224//268 240//284 239//283 223//267
+f 225//269 241//285 240//284 224//268
+f 226//270 242//286 241//285 225//269
+f 227//271 243//287 242//286 226//270
+f 228//272 244//288 259//303 243//287
+f 229//273 245//289 244//288 228//272
+f 230//274 246//290 245//289 229//273
+f 231//275 247//291 246//290 230//274
+f 232//276 248//292 247//291 231//275
+f 233//277 249//293 248//292 232//276
+f 234//278 250//294 249//293 233//277
+f 235//279 251//295 250//294 234//278
+f 236//280 252//296 251//295 235//279
+f 237//281 253//297 252//296 236//280
+f 238//282 254//298 253//297 237//281
+f 239//283 255//299 254//298 238//282
+f 240//284 256//300 255//299 239//283
+f 241//285 257//301 256//300 240//284
+f 242//286 258//302 257//301 241//285
+f 243//287 259//303 258//302 242//286
+f 245//289 246//290 247//291 248//292 249//293 250//294 251//295 252//296 253//297 254//298 255//299 256//300 257//301 258//302 259//303 244//288
+o sphere6_cut7
+#65 vertices, 65 faces
+v 0.82182775 0.54878444 -0.55984640
+v 0.81445656 0.54878444 -0.47285711
+v 0.79346521 0.54878444 -0.39911116
+v 0.76204944 0.54878444 -0.34983569
+v 0.72499200 0.54878444 -0.33253244
+v 0.68793456 0.54878444 -0.34983569
+v 0.65651879 0.54878444 -0.39911116
+v 0.63552744 0.54878444 -0.47285711
+v 0.62815625 0.54878444 -0.55984640
+v 0.63552744 0.54878444 -0.64683569
+v 0.65651879 0.54878444 -0.72058164
+v 0.68793456 0.54878444 -0.76985711
+v 0.72499200 0.54878444 -0.78716036
+v 0.76204944 0.54878444 -0.76985711
+v 0.79346521 0.54878444 -0.72058164
+v 0.81445656 0.54878444 -0.64683569
+v 0.90392113 0.42002143 -0.55984640
+v 0.89030096 0.42002143 -0.39911116
+v 0.85151400 0.42002143 -0.26284640
+v 0.79346521 0.42002143 -0.17179720
+v 0.72499200 0.42002143 -0.13982497
+v 0.65651879 0.42002143 -0.17179720
+v 0.59847000 0.42002143 -0.26284640
+v 0.55968304 0.42002143 -0.39911116
+v 0.54606287 0.42002143 -0.55984640
+v 0.55968304 0.42002143 -0.72058164
+v 0.59847000 0.42002143 -0.85684640
+v 0.65651879 0.42002143 -0.94789560
+v 0.72499200 0.42002143 -0.97986783
+v 0.79346521 0.42002143 -0.94789560
+v 0.85151400 0.42002143 -0.85684640
+v 0.89030096 0.42002143 -0.72058164
+v 0.95877417 0.22731396 -0.55984640
+v 0.94097856 0.22731396 -0.34983569
+v 0.89030096 0.22731396 -0.17179720
+v 0.81445656 0.22731396 -5.2835686e-2
+v 0.72499200 0.22731396 -1.1061958e-2
+v 0.63552744 0.22731396 -5.2835686e-2
+v 0.55968304 0.22731396 -0.17179720
+v 0.50900544 0.22731396 -0.34983569
+v 0.49120983 0.22731396 -0.55984640
+v 0.50900544 0.22731396 -0.76985711
+v 0.55968304 0.22731396 -0.94789560
+v 0.63552744 0.22731396 -1.06685711
+v 0.72499200 0.22731396 -1.10863084
+v 0.81445656 0.22731396 -1.06685711
+v 0.89030096 0.22731396 -0.94789560
+v 0.94097856 0.22731396 -0.76985711
+v 0.97803600 5.7206944e-17 -0.55984640
+v 0.95877417 5.7206944e-17 -0.33253244
+v 0.90392113 5.7206944e-17 -0.13982497
+v 0.82182775 5.7206944e-17 -1.1061958e-2
+v 0.72499200 5.7206944e-17 3.4153600e-2
+v 0.62815625 5.7206944e-17 -1.1061958e-2
+v 0.54606287 5.7206944e-17 -0.13982497
+v 0.49120983 5.7206944e-17 -0.33253244
+v 0.47194800 5.7206944e-17 -0.55984640
+v 0.49120983 5.7206944e-17 -0.78716036
+v 0.54606287 5.7206944e-17 -0.97986783
+v 0.62815625 5.7206944e-17 -1.10863084
+v 0.72499200 5.7206944e-17 -1.15384640
+v 0.82182775 5.7206944e-17 -1.10863084
+v 0.90392113 5.7206944e-17 -0.97986783
+v 0.95877417 5.7206944e-17 -0.78716036
+v 0.72499200 0.59400000 -0.55984640
+vn 0.65920045 0.75196727 5.7906120e-17
+vn 0.62817281 0.76983505 0.11292879
+vn 0.52318843 0.82106426 0.22831414
+vn 0.31317680 0.88932505 0.33319850
+vn 1.1528134e-16 0.92495788 0.38006962
+vn -0.31317680 0.88932505 0.33319850
+vn -0.52318843 0.82106426 0.22831414
+vn -0.62817281 0.76983505 0.11292879
+vn -0.65920045 0.75196727 0.0000000e+0
+vn -0.62817281 0.76983505 -0.11292879
+vn -0.52318843 0.82106426 -0.22831414
+vn -0.31317680 0.88932505 -0.33319850
+vn 2.7379318e-16 0.92495788 -0.38006962
+vn 0.31317680 0.88932505 -0.33319850
+vn 0.52318843 0.82106426 -0.22831414
+vn 0.62817281 0.76983505 -0.11292879
+vn 0.91206716 0.41004084 5.6265464e-17
+vn 0.88721918 0.43227186 0.16119295
+vn 0.78910428 0.50478589 0.35000806
+vn 0.52368375 0.62842774 0.57518164
+vn -1.0425066e-16 0.70974817 0.70445549
+vn -0.52368375 0.62842774 0.57518164
+vn -0.78910428 0.50478589 0.35000806
+vn -0.88721918 0.43227186 0.16119295
+vn -0.91206716 0.41004084 -3.5165915e-18
+vn -0.88721918 0.43227186 -0.16119295
+vn -0.78910428 0.50478589 -0.35000806
+vn -0.52368375 0.62842774 -0.57518164
+vn -1.4892951e-17 0.70974817 -0.70445549
+vn 0.52368375 0.62842774 -0.57518164
+vn 0.78910428 0.50478589 -0.35000806
+vn 0.88721918 0.43227186 -0.16119295
+vn 0.98367604 0.17994844 -7.3449723e-17
+vn 0.96545175 0.19187092 0.17631923
+vn 0.88733269 0.23376762 0.39748383
+vn 0.62911347 0.31848577 0.70907197
+vn -2.3052462e-16 0.38497918 0.92292526
+vn -0.62911347 0.31848577 0.70907197
+vn -0.88733269 0.23376762 0.39748383
+vn -0.96545175 0.19187092 0.17631923
+vn -0.98367604 0.17994844 -1.3990423e-17
+vn -0.96545175 0.19187092 -0.17631923
+vn -0.88733269 0.23376762 -0.39748383
+vn -0.62911347 0.31848577 -0.70907197
+vn -1.9978801e-16 0.38497918 -0.92292526
+vn 0.62911347 0.31848577 -0.70907197
+vn 0.88733269 0.23376762 -0.39748383
+vn 0.96545175 0.19187092 -0.17631923
+vn 0.92236342 -0.38632333 -1.9337920e-16
+vn 0.90897968 -0.38226311 0.16622532
+vn 0.84805657 -0.36833221 0.38096120
+vn 0.61975352 -0.34499831 0.70489839
+vn -2.3415204e-16 -0.34021686 0.94034700
+vn -0.61975352 -0.34499831 0.70489839
+vn -0.84805657 -0.36833221 0.38096120
+vn -0.90897968 -0.38226311 0.16622532
+vn -0.92236342 -0.38632333 -1.9337920e-17
+vn -0.90897968 -0.38226311 -0.16622532
+vn -0.84805657 -0.36833221 -0.38096120
+vn -0.61975352 -0.34499831 -0.70489839
+vn -3.5122805e-16 -0.34021686 -0.94034700
+vn 0.61975352 -0.34499831 -0.70489839
+vn 0.84805657 -0.36833221 -0.38096120
+vn 0.90897968 -0.38226311 -0.16622532
+vn 9.5934020e-17 1.00000000 -7.1028072e-17
+g sphere6_cut7_default
+usemtl default
+f 261//305 277//321 292//336 276//320
+f 261//305 325//369 262//306
+f 262//306 278//322 277//321 261//305
+f 262//306 325//369 263//307
+f 263//307 279//323 278//322 262//306
+f 263//307 325//369 264//308
+f 264//308 280//324 279//323 263//307
+f 264//308 325//369 265//309
+f 265//309 281//325 280//324 264//308
+f 265//309 325//369 266//310
+f 266//310 282//326 281//325 265//309
+f 266//310 325//369 267//311
+f 267//311 283//327 282//326 266//310
+f 267//311 325//369 268//312
+f 268//312 284//328 283//327 267//311
+f 268//312 325//369 269//313
+f 269//313 285//329 284//328 268//312
+f 269//313 325//369 270//314
+f 270//314 286//330 285//329 269//313
+f 270//314 325//369 271//315
+f 271//315 287//331 286//330 270//314
+f 271//315 325//369 272//316
+f 272//316 288//332 287//331 271//315
+f 272//316 325//369 273//317
+f 273//317 289//333 288//332 272//316
+f 273//317 325//369 274//318
+f 274//318 290//334 289//333 273//317
+f 274//318 325//369 275//319
+f 275//319 291//335 290//334 274//318
+f 275//319 325//369 276//320
+f 276//320 292//336 291//335 275//319
+f 276//320 325//369 261//305
+f 277//321 293//337 308//352 292//336
+f 278//322 294//338 293//337 277//321
+f 279//323 295//339 294//338 278//322
+f 280//324 296//340 295//339 279//323
+f 281//325 297//341 296//340 280//324
+f 282//326 298//342 297//341 281//325
+f 283//327 299//343 298//342 282//326
+f 284//328 300//344 299//343 283//327
+f 285//329 301//345 300//344 284//328
+f 286//330 302//346 301//345 285//329
+f 287//331 303//347 302//346 286//330
+f 288//332 304//348 303//347 287//331
+f 289//333 305//349 304//348 288//332
+f 290//334 306//350 305//349 289//333
+f 291//335 307//351 306//350 290//334
+f 292//336 308//352 307//351 291//335
+f 293//337 309//353 324//368 308//352
+f 294//338 310//354 309//353 293//337
+f 295//339 311//355 310//354 294//338
+f 296//340 312//356 311//355 295//339
+f 297//341 313//357 312//356 296//340
+f 298//342 314//358 313//357 297//341
+f 299//343 315//359 314//358 298//342
+f 300//344 316//360 315//359 299//343
+f 301//345 317//361 316//360 300//344
+f 302//346 318//362 317//361 301//345
+f 303//347 319//363 318//362 302//346
+f 304//348 320//364 319//363 303//347
+f 305//349 321//365 320//364 304//348
+f 306//350 322//366 321//365 305//349
+f 307//351 323//367 322//366 306//350
+f 308//352 324//368 323//367 307//351
+f 310//354 311//355 312//356 313//357 314//358 315//359 316//360 317//361 318//362 319//363 320//364 321//365 322//366 323//367 324//368 309//353
+o sphere4
+#65 vertices, 65 faces
+v 0.51118080 4.33748593 1.00287825
+v 0.48599198 4.33748593 0.98924539
+v 0.46463794 4.33748593 0.95042229
+v 0.45036962 4.33748593 0.89231941
+v 0.44535925 4.33748593 0.82378240
+v 0.45036962 4.33748593 0.75524539
+v 0.46463794 4.33748593 0.69714251
+v 0.48599198 4.33748593 0.65831941
+v 0.51118080 4.33748593 0.64468655
+v 0.51118080 4.12071318 1.15470837
+v 0.46463794 4.12071318 1.12951813
+v 0.42518080 4.12071318 1.05778240
+v 0.39881639 4.12071318 0.95042229
+v 0.38955843 4.12071318 0.82378240
+v 0.39881639 4.12071318 0.69714251
+v 0.42518080 4.12071318 0.58978240
+v 0.46463794 4.12071318 0.51804667
+v 0.51118080 4.12071318 0.49285643
+v 0.51118080 3.79628983 1.25615802
+v 0.45036962 3.79628983 1.22324539
+v 0.39881639 3.79628983 1.12951813
+v 0.36436962 3.79628983 0.98924539
+v 0.35227352 3.79628983 0.82378240
+v 0.36436962 3.79628983 0.65831941
+v 0.39881639 3.79628983 0.51804667
+v 0.45036962 3.79628983 0.42431941
+v 0.51118080 3.79628983 0.39140678
+v 0.51118080 3.41360640 1.29178240
+v 0.44535925 3.41360640 1.25615802
+v 0.38955843 3.41360640 1.15470837
+v 0.35227352 3.41360640 1.00287825
+v 0.33918080 3.41360640 0.82378240
+v 0.35227352 3.41360640 0.64468655
+v 0.38955843 3.41360640 0.49285643
+v 0.44535925 3.41360640 0.39140678
+v 0.51118080 3.41360640 0.35578240
+v 0.51118080 3.03092297 1.25615802
+v 0.45036962 3.03092297 1.22324539
+v 0.39881639 3.03092297 1.12951813
+v 0.36436962 3.03092297 0.98924539
+v 0.35227352 3.03092297 0.82378240
+v 0.36436962 3.03092297 0.65831941
+v 0.39881639 3.03092297 0.51804667
+v 0.45036962 3.03092297 0.42431941
+v 0.51118080 3.03092297 0.39140678
+v 0.51118080 2.70649962 1.15470837
+v 0.46463794 2.70649962 1.12951813
+v 0.42518080 2.70649962 1.05778240
+v 0.39881639 2.70649962 0.95042229
+v 0.38955843 2.70649962 0.82378240
+v 0.39881639 2.70649962 0.69714251
+v 0.42518080 2.70649962 0.58978240
+v 0.46463794 2.70649962 0.51804667
+v 0.51118080 2.70649962 0.49285643
+v 0.51118080 2.48972687 1.00287825
+v 0.48599198 2.48972687 0.98924539
+v 0.46463794 2.48972687 0.95042229
+v 0.45036962 2.48972687 0.89231941
+v 0.44535925 2.48972687 0.82378240
+v 0.45036962 2.48972687 0.75524539
+v 0.46463794 2.48972687 0.69714251
+v 0.48599198 2.48972687 0.65831941
+v 0.51118080 2.48972687 0.64468655
+v 0.51118080 4.41360640 0.82378240
+v 0.51118080 2.41360640 0.82378240
+vn -0.52083846 0.69290596 0.49860670
+vn -0.77196053 0.56262068 0.29586299
+vn -0.86267949 0.48733515 0.13523516
+vn -0.88580462 0.46405838 -4.6421556e-17
+vn -0.86267949 0.48733515 -0.13523516
+vn -0.77196053 0.56262068 -0.29586299
+vn -0.52083846 0.69290596 -0.49860670
+vn -0.66538177 0.34948186 0.65964349
+vn -0.90425783 0.24266597 0.35132749
+vn -0.96875968 0.19517878 0.15300305
+vn -0.98326758 0.18216714 -2.4406179e-17
+vn -0.96875968 0.19517878 -0.15300305
+vn -0.90425783 0.24266597 -0.35132749
+vn -0.66538177 0.34948186 -0.65964349
+vn -0.69787980 0.15014160 0.70030086
+vn -0.92704382 0.10040034 0.36126103
+vn -0.98457777 7.9770475e-2 0.15570256
+vn -0.99724008 7.4244402e-2 0.0000000e+0
+vn -0.98457777 7.9770475e-2 -0.15570256
+vn -0.92704382 0.10040034 -0.36126103
+vn -0.69787980 0.15014160 -0.70030086
+vn -0.70490553 6.5247046e-17 0.70930120
+vn -0.93167648 5.9555034e-17 0.36328906
+vn -0.98771924 -8.7101199e-18 0.15623925
+vn -1.00000000 0.0000000e+0 0.0000000e+0
+vn -0.98771924 -5.2260720e-18 -0.15623925
+vn -0.93167648 -3.5032373e-18 -0.36328906
+vn -0.70490553 2.1749015e-17 -0.70930120
+vn -0.69787980 -0.15014160 0.70030086
+vn -0.92704382 -0.10040034 0.36126103
+vn -0.98457777 -7.9770475e-2 0.15570256
+vn -0.99724008 -7.4244402e-2 1.4622049e-16
+vn -0.98457777 -7.9770475e-2 -0.15570256
+vn -0.92704382 -0.10040034 -0.36126103
+vn -0.69787980 -0.15014160 -0.70030086
+vn -0.66538177 -0.34948186 0.65964349
+vn -0.90425783 -0.24266597 0.35132749
+vn -0.96875968 -0.19517878 0.15300305
+vn -0.98326758 -0.18216714 3.1728033e-16
+vn -0.96875968 -0.19517878 -0.15300305
+vn -0.90425783 -0.24266597 -0.35132749
+vn -0.66538177 -0.34948186 -0.65964349
+vn -0.52083846 -0.69290596 0.49860670
+vn -0.77196053 -0.56262068 0.29586299
+vn -0.86267949 -0.48733515 0.13523516
+vn -0.88580462 -0.46405838 1.5354822e-16
+vn -0.86267949 -0.48733515 -0.13523516
+vn -0.77196053 -0.56262068 -0.29586299
+vn -0.52083846 -0.69290596 -0.49860670
+vn -0.31899373 0.74220086 0.58939026
+vn -0.31899373 0.74220086 0.58939026
+vn 1.00000000 1.9372012e-17 -1.2638195e-16
+vn -0.31899373 0.74220086 -0.58939026
+vn -0.31899373 0.74220086 -0.58939026
+vn 1.00000000 1.9372012e-17 -1.2638195e-16
+vn -0.43649078 0.39882199 0.80648423
+vn -0.43649078 0.39882199 0.80648423
+vn 1.00000000 1.9372012e-17 -1.2638195e-16
+vn -0.43649078 0.39882199 -0.80648423
+vn -0.43649078 0.39882199 -0.80648423
+vn 1.00000000 1.9372012e-17 -1.2638195e-16
+vn -0.46871136 0.17413983 0.86601673
+vn -0.46871136 0.17413983 0.86601673
+vn 1.00000000 1.9372012e-17 -1.2638195e-16
+vn -0.46871136 0.17413983 -0.86601673
+vn -0.46871136 0.17413983 -0.86601673
+vn 1.00000000 1.9372012e-17 -1.2638195e-16
+vn -0.47598395 -1.3924219e-17 0.87945397
+vn -0.47598395 -1.3924219e-17 0.87945397
+vn 1.00000000 1.9372012e-17 -1.2638195e-16
+vn -0.47598395 4.8734765e-17 -0.87945397
+vn -0.47598395 4.8734765e-17 -0.87945397
+vn 1.00000000 1.9372012e-17 -1.2638195e-16
+vn -0.46871136 -0.17413983 0.86601673
+vn -0.46871136 -0.17413983 0.86601673
+vn 1.00000000 1.9372012e-17 -1.2638195e-16
+vn -0.46871136 -0.17413983 -0.86601673
+vn -0.46871136 -0.17413983 -0.86601673
+vn 1.00000000 1.9372012e-17 -1.2638195e-16
+vn -0.43649078 -0.39882199 0.80648423
+vn -0.43649078 -0.39882199 0.80648423
+vn 1.00000000 1.9372012e-17 -1.2638195e-16
+vn -0.43649078 -0.39882199 -0.80648423
+vn -0.43649078 -0.39882199 -0.80648423
+vn 1.00000000 1.9372012e-17 -1.2638195e-16
+vn -0.31899373 -0.74220086 0.58939026
+vn -0.31899373 -0.74220086 0.58939026
+vn 1.00000000 1.9372012e-17 -1.2638195e-16
+vn -0.31899373 -0.74220086 -0.58939026
+vn -0.31899373 -0.74220086 -0.58939026
+vn 1.00000000 1.9372012e-17 -1.2638195e-16
+vn -0.57994027 0.81465900 -2.9575661e-17
+vn -0.57994027 0.81465900 -3.6969576e-17
+vn -0.57994027 0.81465900 -3.3272618e-17
+vn -0.57994027 0.81465900 -3.6969576e-17
+vn -0.57994027 0.81465900 -4.7136209e-17
+vn -0.57994027 0.81465900 -4.0666533e-17
+vn -0.57994027 0.81465900 -3.6969576e-17
+vn -0.57994027 0.81465900 -3.6969576e-17
+vn 1.00000000 1.9372012e-17 -1.2638195e-16
+vn -0.57994027 -0.81465900 1.1090873e-16
+vn -0.57994027 -0.81465900 1.1830264e-16
+vn -0.57994027 -0.81465900 1.1830264e-16
+vn -0.57994027 -0.81465900 1.2107536e-16
+vn -0.57994027 -0.81465900 1.2754504e-16
+vn -0.57994027 -0.81465900 1.1830264e-16
+vn -0.57994027 -0.81465900 1.1830264e-16
+vn -0.57994027 -0.81465900 1.1830264e-16
+vn 1.00000000 1.9372012e-17 -1.2638195e-16
+g sphere4_default
+usemtl default
+f 326//419 389//461 327//370
+f 327//370 336//377 335//425 326//420
+f 327//370 389//462 328//371
+f 328//371 337//378 336//377 327//370
+f 328//371 389//463 329//372
+f 329//372 338//379 337//378 328//371
+f 329//372 389//464 330//373
+f 330//373 339//380 338//379 329//372
+f 330//373 389//465 331//374
+f 331//374 340//381 339//380 330//373
+f 331//374 389//466 332//375
+f 332//375 341//382 340//381 331//374
+f 332//375 389//467 333//376
+f 333//376 342//383 341//382 332//375
+f 333//376 389//468 334//422
+f 334//423 343//428 342//383 333//376
+f 335//427 344//433 353//439 362//445 371//451 380//457 390//478 388//460 379//454 370//448 361//442 352//436 343//430 334//424 389//469 326//421
+f 336//377 345//384 344//431 335//426
+f 337//378 346//385 345//384 336//377
+f 338//379 347//386 346//385 337//378
+f 339//380 348//387 347//386 338//379
+f 340//381 349//388 348//387 339//380
+f 341//382 350//389 349//388 340//381
+f 342//383 351//390 350//389 341//382
+f 343//429 352//434 351//390 342//383
+f 345//384 354//391 353//437 344//432
+f 346//385 355//392 354//391 345//384
+f 347//386 356//393 355//392 346//385
+f 348//387 357//394 356//393 347//386
+f 349//388 358//395 357//394 348//387
+f 350//389 359//396 358//395 349//388
+f 351//390 360//397 359//396 350//389
+f 352//435 361//440 360//397 351//390
+f 354//391 363//398 362//443 353//438
+f 355//392 364//399 363//398 354//391
+f 356//393 365//400 364//399 355//392
+f 357//394 366//401 365//400 356//393
+f 358//395 367//402 366//401 357//394
+f 359//396 368//403 367//402 358//395
+f 360//397 369//404 368//403 359//396
+f 361//441 370//446 369//404 360//397
+f 363//398 372//405 371//449 362//444
+f 364//399 373//406 372//405 363//398
+f 365//400 374//407 373//406 364//399
+f 366//401 375//408 374//407 365//400
+f 367//402 376//409 375//408 366//401
+f 368//403 377//410 376//409 367//402
+f 369//404 378//411 377//410 368//403
+f 370//447 379//452 378//411 369//404
+f 372//405 381//412 380//456 371//450
+f 373//406 382//413 381//412 372//405
+f 374//407 383//414 382//413 373//406
+f 375//408 384//415 383//414 374//407
+f 376//409 385//416 384//415 375//408
+f 377//410 386//417 385//416 376//409
+f 378//411 387//418 386//417 377//410
+f 379//453 388//459 387//418 378//411
+f 381//412 390//470 380//455
+f 382//413 390//471 381//412
+f 383//414 390//472 382//413
+f 384//415 390//473 383//414
+f 385//416 390//474 384//415
+f 386//417 390//475 385//416
+f 387//418 390//476 386//417
+f 388//458 390//477 387//418
+o sphere3
+#26 vertices, 32 faces
+v 0.25031580 0.66098247 -1.07200000
+v 0.17700000 0.66098247 -0.89500000
+v -6.3562636e-17 0.66098247 -0.82168420
+v -0.17700000 0.66098247 -0.89500000
+v -0.25031580 0.66098247 -1.07200000
+v -0.17700000 0.66098247 -1.24900000
+v -1.2487030e-16 0.66098247 -1.32231580
+v 0.17700000 0.66098247 -1.24900000
+v 0.35400000 0.41066667 -1.07200000
+v 0.25031580 0.41066667 -0.82168420
+v -5.7214020e-17 0.41066667 -0.71800000
+v -0.25031580 0.41066667 -0.82168420
+v -0.35400000 0.41066667 -1.07200000
+v -0.25031580 0.41066667 -1.32231580
+v -1.4391615e-16 0.41066667 -1.42600000
+v 0.25031580 0.41066667 -1.32231580
+v 0.25031580 0.16035087 -1.07200000
+v 0.17700000 0.16035087 -0.89500000
+v -6.3562636e-17 0.16035087 -0.82168420
+v -0.17700000 0.16035087 -0.89500000
+v -0.25031580 0.16035087 -1.07200000
+v -0.17700000 0.16035087 -1.24900000
+v -1.2487030e-16 0.16035087 -1.32231580
+v 0.17700000 0.16035087 -1.24900000
+v -7.8889552e-17 0.76466667 -1.07200000
+v -7.8889552e-17 5.6666667e-2 -1.07200000
+vn 0.69887193 0.71524683 3.1265747e-17
+vn 0.49417708 0.71524683 0.49417708
+vn -3.1265747e-17 0.71524683 0.69887193
+vn -0.49417708 0.71524683 0.49417708
+vn -0.69887193 0.71524683 -1.4069586e-16
+vn -0.49417708 0.71524683 -0.49417708
+vn -5.4715057e-16 0.71524683 -0.69887193
+vn 0.49417708 0.71524683 -0.49417708
+vn 1.00000000 1.6083546e-17 -1.2866837e-16
+vn 0.70710678 1.4475192e-16 0.70710678
+vn -1.6083546e-17 1.6083546e-16 1.00000000
+vn -0.70710678 1.1258482e-16 0.70710678
+vn -1.00000000 1.9300256e-16 -6.4334185e-17
+vn -0.70710678 8.0417732e-17 -0.70710678
+vn -4.5033930e-16 1.4475192e-16 -1.00000000
+vn 0.70710678 1.1258482e-16 -0.70710678
+vn 0.69887193 -0.71524683 -7.8164367e-17
+vn 0.49417708 -0.71524683 0.49417708
+vn 1.5632873e-17 -0.71524683 0.69887193
+vn -0.49417708 -0.71524683 0.49417708
+vn -0.69887193 -0.71524683 -9.3797240e-17
+vn -0.49417708 -0.71524683 -0.49417708
+vn -5.0025195e-16 -0.71524683 -0.69887193
+vn 0.49417708 -0.71524683 -0.49417708
+vn -7.6043760e-17 1.00000000 -1.1406564e-16
+vn -7.6043760e-17 -1.00000000 -1.0646126e-16
+g sphere3_default
+usemtl default
+f 391//479 399//487 406//494 398//486
+f 391//479 415//503 392//480
+f 392//480 400//488 399//487 391//479
+f 392//480 415//503 393//481
+f 393//481 401//489 400//488 392//480
+f 393//481 415//503 394//482
+f 394//482 402//490 401//489 393//481
+f 394//482 415//503 395//483
+f 395//483 403//491 402//490 394//482
+f 395//483 415//503 396//484
+f 396//484 404//492 403//491 395//483
+f 396//484 415//503 397//485
+f 397//485 405//493 404//492 396//484
+f 397//485 415//503 398//486
+f 398//486 406//494 405//493 397//485
+f 398//486 415//503 391//479
+f 399//487 407//495 414//502 406//494
+f 400//488 408//496 407//495 399//487
+f 401//489 409//497 408//496 400//488
+f 402//490 410//498 409//497 401//489
+f 403//491 411//499 410//498 402//490
+f 404//492 412//500 411//499 403//491
+f 405//493 413//501 412//500 404//492
+f 406//494 414//502 413//501 405//493
+f 407//495 416//504 414//502
+f 408//496 416//504 407//495
+f 409//497 416//504 408//496
+f 410//498 416//504 409//497
+f 411//499 416//504 410//498
+f 412//500 416//504 411//499
+f 413//501 416//504 412//500
+f 414//502 416//504 413//501
+o sphere2
+#114 vertices, 128 faces
+v 0.38268343 1.93010175 3.5555556e-3
+v 0.35355339 1.93010175 0.15000216
+v 0.27059805 1.93010175 0.27415361
+v 0.14644661 1.93010175 0.35710895
+v -9.9246515e-17 1.93010175 0.38623899
+v -0.14644661 1.93010175 0.35710895
+v -0.27059805 1.93010175 0.27415361
+v -0.35355339 1.93010175 0.15000216
+v -0.38268343 1.93010175 3.5555556e-3
+v -0.35355339 1.93010175 -0.14289105
+v -0.27059805 1.93010175 -0.26704249
+v -0.14644661 1.93010175 -0.34999784
+v -1.9297383e-16 1.93010175 -0.37912788
+v 0.14644661 1.93010175 -0.34999784
+v 0.27059805 1.93010175 -0.26704249
+v 0.35355339 1.93010175 -0.14289105
+v 0.70710678 1.71332900 3.5555556e-3
+v 0.65328148 1.71332900 0.27415361
+v 0.50000000 1.71332900 0.50355556
+v 0.27059805 1.71332900 0.65683704
+v -7.9381970e-17 1.71332900 0.71066234
+v -0.27059805 1.71332900 0.65683704
+v -0.50000000 1.71332900 0.50355556
+v -0.65328148 1.71332900 0.27415361
+v -0.70710678 1.71332900 3.5555556e-3
+v -0.65328148 1.71332900 -0.26704249
+v -0.50000000 1.71332900 -0.49644444
+v -0.27059805 1.71332900 -0.64972593
+v -2.5256746e-16 1.71332900 -0.70355123
+v 0.27059805 1.71332900 -0.64972593
+v 0.50000000 1.71332900 -0.49644444
+v 0.65328148 1.71332900 -0.26704249
+v 0.92387953 1.38890565 3.5555556e-3
+v 0.85355339 1.38890565 0.35710895
+v 0.65328148 1.38890565 0.65683704
+v 0.35355339 1.38890565 0.85710895
+v -6.6108906e-17 1.38890565 0.92743509
+v -0.35355339 1.38890565 0.85710895
+v -0.65328148 1.38890565 0.65683704
+v -0.85355339 1.38890565 0.35710895
+v -0.92387953 1.38890565 3.5555556e-3
+v -0.85355339 1.38890565 -0.34999784
+v -0.65328148 1.38890565 -0.64972593
+v -0.35355339 1.38890565 -0.84999784
+v -2.9238666e-16 1.38890565 -0.92032398
+v 0.35355339 1.38890565 -0.84999784
+v 0.65328148 1.38890565 -0.64972593
+v 0.85355339 1.38890565 -0.34999784
+v 1.00000000 1.00622222 3.5555556e-3
+v 0.92387953 1.00622222 0.38623899
+v 0.70710678 1.00622222 0.71066234
+v 0.38268343 1.00622222 0.92743509
+v -6.1448025e-17 1.00622222 1.00355556
+v -0.38268343 1.00622222 0.92743509
+v -0.70710678 1.00622222 0.71066234
+v -0.92387953 1.00622222 0.38623899
+v -1.00000000 1.00622222 3.5555556e-3
+v -0.92387953 1.00622222 -0.37912788
+v -0.70710678 1.00622222 -0.70355123
+v -0.38268343 1.00622222 -0.92032398
+v -3.0636930e-16 1.00622222 -0.99644444
+v 0.38268343 1.00622222 -0.92032398
+v 0.70710678 1.00622222 -0.70355123
+v 0.92387953 1.00622222 -0.37912788
+v 0.92387953 0.62353879 3.5555556e-3
+v 0.85355339 0.62353879 0.35710895
+v 0.65328148 0.62353879 0.65683704
+v 0.35355339 0.62353879 0.85710895
+v -6.6108906e-17 0.62353879 0.92743509
+v -0.35355339 0.62353879 0.85710895
+v -0.65328148 0.62353879 0.65683704
+v -0.85355339 0.62353879 0.35710895
+v -0.92387953 0.62353879 3.5555556e-3
+v -0.85355339 0.62353879 -0.34999784
+v -0.65328148 0.62353879 -0.64972593
+v -0.35355339 0.62353879 -0.84999784
+v -2.9238666e-16 0.62353879 -0.92032398
+v 0.35355339 0.62353879 -0.84999784
+v 0.65328148 0.62353879 -0.64972593
+v 0.85355339 0.62353879 -0.34999784
+v 0.70710678 0.29911544 3.5555556e-3
+v 0.65328148 0.29911544 0.27415361
+v 0.50000000 0.29911544 0.50355556
+v 0.27059805 0.29911544 0.65683704
+v -7.9381970e-17 0.29911544 0.71066234
+v -0.27059805 0.29911544 0.65683704
+v -0.50000000 0.29911544 0.50355556
+v -0.65328148 0.29911544 0.27415361
+v -0.70710678 0.29911544 3.5555556e-3
+v -0.65328148 0.29911544 -0.26704249
+v -0.50000000 0.29911544 -0.49644444
+v -0.27059805 0.29911544 -0.64972593
+v -2.5256746e-16 0.29911544 -0.70355123
+v 0.27059805 0.29911544 -0.64972593
+v 0.50000000 0.29911544 -0.49644444
+v 0.65328148 0.29911544 -0.26704249
+v 0.38268343 8.2342690e-2 3.5555556e-3
+v 0.35355339 8.2342690e-2 0.15000216
+v 0.27059805 8.2342690e-2 0.27415361
+v 0.14644661 8.2342690e-2 0.35710895
+v -9.9246515e-17 8.2342690e-2 0.38623899
+v -0.14644661 8.2342690e-2 0.35710895
+v -0.27059805 8.2342690e-2 0.27415361
+v -0.35355339 8.2342690e-2 0.15000216
+v -0.38268343 8.2342690e-2 3.5555556e-3
+v -0.35355339 8.2342690e-2 -0.14289105
+v -0.27059805 8.2342690e-2 -0.26704249
+v -0.14644661 8.2342690e-2 -0.34999784
+v -1.9297383e-16 8.2342690e-2 -0.37912788
+v 0.14644661 8.2342690e-2 -0.34999784
+v 0.27059805 8.2342690e-2 -0.26704249
+v 0.35355339 8.2342690e-2 -0.14289105
+v -1.2267834e-16 2.00622222 3.5555556e-3
+v -1.2267834e-16 6.2222222e-3 3.5555556e-3
+vn 0.38219484 0.92408176 -1.0648471e-17
+vn 0.35310199 0.92408176 0.14625963
+vn 0.27025256 0.92408176 0.27025256
+vn 0.14625963 0.92408176 0.35310199
+vn 0.0000000e+0 0.92408176 0.38219484
+vn -0.14625963 0.92408176 0.35310199
+vn -0.27025256 0.92408176 0.27025256
+vn -0.35310199 0.92408176 0.14625963
+vn -0.38219484 0.92408176 0.0000000e+0
+vn -0.35310199 0.92408176 -0.14625963
+vn -0.27025256 0.92408176 -0.27025256
+vn -0.14625963 0.92408176 -0.35310199
+vn 1.8102401e-16 0.92408176 -0.38219484
+vn 0.14625963 0.92408176 -0.35310199
+vn 0.27025256 0.92408176 -0.27025256
+vn 0.35310199 0.92408176 -0.14625963
+vn 0.70658450 0.70762868 -4.2865941e-17
+vn 0.65279895 0.70762868 0.27039818
+vn 0.49963069 0.70762868 0.49963069
+vn 0.27039818 0.70762868 0.65279895
+vn -1.4288647e-17 0.70762868 0.70658450
+vn -0.27039818 0.70762868 0.65279895
+vn -0.49963069 0.70762868 0.49963069
+vn -0.65279895 0.70762868 0.27039818
+vn -0.70658450 0.70762868 3.5721617e-17
+vn -0.65279895 0.70762868 -0.27039818
+vn -0.49963069 0.70762868 -0.49963069
+vn -0.27039818 0.70762868 -0.65279895
+vn 1.7860809e-16 0.70762868 -0.70658450
+vn 0.27039818 0.70762868 -0.65279895
+vn 0.49963069 0.70762868 -0.49963069
+vn 0.65279895 0.70762868 -0.27039818
+vn 0.92368212 0.38315969 -1.6535916e-16
+vn 0.85337100 0.38315969 0.35347784
+vn 0.65314189 0.38315969 0.65314189
+vn 0.35347784 0.38315969 0.85337100
+vn 0.0000000e+0 0.38315969 0.92368212
+vn -0.35347784 0.38315969 0.85337100
+vn -0.65314189 0.38315969 0.65314189
+vn -0.85337100 0.38315969 0.35347784
+vn -0.92368212 0.38315969 7.9084817e-17
+vn -0.85337100 0.38315969 -0.35347784
+vn -0.65314189 0.38315969 -0.65314189
+vn -0.35347784 0.38315969 -0.85337100
+vn 0.0000000e+0 0.38315969 -0.92368212
+vn 0.35347784 0.38315969 -0.85337100
+vn 0.65314189 0.38315969 -0.65314189
+vn 0.85337100 0.38315969 -0.35347784
+vn 1.00000000 -7.2082126e-18 -2.0903816e-16
+vn 0.92387953 -7.2082126e-18 0.38268343
+vn 0.70710678 -1.4416425e-17 0.70710678
+vn 0.38268343 -1.4416425e-17 0.92387953
+vn 7.2082126e-18 0.0000000e+0 1.00000000
+vn -0.38268343 -7.2082126e-18 0.92387953
+vn -0.70710678 0.0000000e+0 0.70710678
+vn -0.92387953 7.2082126e-18 0.38268343
+vn -1.00000000 -7.2082126e-18 1.1533140e-16
+vn -0.92387953 0.0000000e+0 -0.38268343
+vn -0.70710678 5.0457488e-17 -0.70710678
+vn -0.38268343 1.4416425e-16 -0.92387953
+vn 0.0000000e+0 5.0457488e-17 -1.00000000
+vn 0.38268343 -7.2082126e-17 -0.92387953
+vn 0.70710678 -7.2082126e-18 -0.70710678
+vn 0.92387953 2.8832850e-17 -0.38268343
+vn 0.92368212 -0.38315969 -2.3006492e-16
+vn 0.85337100 -0.38315969 0.35347784
+vn 0.65314189 -0.38315969 0.65314189
+vn 0.35347784 -0.38315969 0.85337100
+vn 1.4379058e-17 -0.38315969 0.92368212
+vn -0.35347784 -0.38315969 0.85337100
+vn -0.65314189 -0.38315969 0.65314189
+vn -0.85337100 -0.38315969 0.35347784
+vn -0.92368212 -0.38315969 7.9084817e-17
+vn -0.85337100 -0.38315969 -0.35347784
+vn -0.65314189 -0.38315969 -0.65314189
+vn -0.35347784 -0.38315969 -0.85337100
+vn 0.0000000e+0 -0.38315969 -0.92368212
+vn 0.35347784 -0.38315969 -0.85337100
+vn 0.65314189 -0.38315969 -0.65314189
+vn 0.85337100 -0.38315969 -0.35347784
+vn 0.70658450 -0.70762868 -1.9646890e-16
+vn 0.65279895 -0.70762868 0.27039818
+vn 0.49963069 -0.70762868 0.49963069
+vn 0.27039818 -0.70762868 0.65279895
+vn 3.2149456e-17 -0.70762868 0.70658450
+vn -0.27039818 -0.70762868 0.65279895
+vn -0.49963069 -0.70762868 0.49963069
+vn -0.65279895 -0.70762868 0.27039818
+vn -0.70658450 -0.70762868 8.2159720e-17
+vn -0.65279895 -0.70762868 -0.27039818
+vn -0.49963069 -0.70762868 -0.49963069
+vn -0.27039818 -0.70762868 -0.65279895
+vn -6.0726750e-17 -0.70762868 -0.70658450
+vn 0.27039818 -0.70762868 -0.65279895
+vn 0.49963069 -0.70762868 -0.49963069
+vn 0.65279895 -0.70762868 -0.27039818
+vn 0.38219484 -0.92408176 -8.1638280e-17
+vn 0.35310199 -0.92408176 0.14625963
+vn 0.27025256 -0.92408176 0.27025256
+vn 0.14625963 -0.92408176 0.35310199
+vn 1.7747452e-17 -0.92408176 0.38219484
+vn -0.14625963 -0.92408176 0.35310199
+vn -0.27025256 -0.92408176 0.27025256
+vn -0.35310199 -0.92408176 0.14625963
+vn -0.38219484 -0.92408176 8.1638280e-17
+vn -0.35310199 -0.92408176 -0.14625963
+vn -0.27025256 -0.92408176 -0.27025256
+vn -0.14625963 -0.92408176 -0.35310199
+vn -1.3843013e-16 -0.92408176 -0.38219484
+vn 0.14625963 -0.92408176 -0.35310199
+vn 0.27025256 -0.92408176 -0.27025256
+vn 0.35310199 -0.92408176 -0.14625963
+vn 3.1860719e-17 1.00000000 -1.7700400e-18
+vn -8.8501998e-18 -1.00000000 -1.5045340e-17
+g sphere2_default
+usemtl default
+f 417//505 433//521 448//536 432//520
+f 417//505 529//617 418//506
+f 418//506 434//522 433//521 417//505
+f 418//506 529//617 419//507
+f 419//507 435//523 434//522 418//506
+f 419//507 529//617 420//508
+f 420//508 436//524 435//523 419//507
+f 420//508 529//617 421//509
+f 421//509 437//525 436//524 420//508
+f 421//509 529//617 422//510
+f 422//510 438//526 437//525 421//509
+f 422//510 529//617 423//511
+f 423//511 439//527 438//526 422//510
+f 423//511 529//617 424//512
+f 424//512 440//528 439//527 423//511
+f 424//512 529//617 425//513
+f 425//513 441//529 440//528 424//512
+f 425//513 529//617 426//514
+f 426//514 442//530 441//529 425//513
+f 426//514 529//617 427//515
+f 427//515 443//531 442//530 426//514
+f 427//515 529//617 428//516
+f 428//516 444//532 443//531 427//515
+f 428//516 529//617 429//517
+f 429//517 445//533 444//532 428//516
+f 429//517 529//617 430//518
+f 430//518 446//534 445//533 429//517
+f 430//518 529//617 431//519
+f 431//519 447//535 446//534 430//518
+f 431//519 529//617 432//520
+f 432//520 448//536 447//535 431//519
+f 432//520 529//617 417//505
+f 433//521 449//537 464//552 448//536
+f 434//522 450//538 449//537 433//521
+f 435//523 451//539 450//538 434//522
+f 436//524 452//540 451//539 435//523
+f 437//525 453//541 452//540 436//524
+f 438//526 454//542 453//541 437//525
+f 439//527 455//543 454//542 438//526
+f 440//528 456//544 455//543 439//527
+f 441//529 457//545 456//544 440//528
+f 442//530 458//546 457//545 441//529
+f 443//531 459//547 458//546 442//530
+f 444//532 460//548 459//547 443//531
+f 445//533 461//549 460//548 444//532
+f 446//534 462//550 461//549 445//533
+f 447//535 463//551 462//550 446//534
+f 448//536 464//552 463//551 447//535
+f 449//537 465//553 480//568 464//552
+f 450//538 466//554 465//553 449//537
+f 451//539 467//555 466//554 450//538
+f 452//540 468//556 467//555 451//539
+f 453//541 469//557 468//556 452//540
+f 454//542 470//558 469//557 453//541
+f 455//543 471//559 470//558 454//542
+f 456//544 472//560 471//559 455//543
+f 457//545 473//561 472//560 456//544
+f 458//546 474//562 473//561 457//545
+f 459//547 475//563 474//562 458//546
+f 460//548 476//564 475//563 459//547
+f 461//549 477//565 476//564 460//548
+f 462//550 478//566 477//565 461//549
+f 463//551 479//567 478//566 462//550
+f 464//552 480//568 479//567 463//551
+f 465//553 481//569 496//584 480//568
+f 466//554 482//570 481//569 465//553
+f 467//555 483//571 482//570 466//554
+f 468//556 484//572 483//571 467//555
+f 469//557 485//573 484//572 468//556
+f 470//558 486//574 485//573 469//557
+f 471//559 487//575 486//574 470//558
+f 472//560 488//576 487//575 471//559
+f 473//561 489//577 488//576 472//560
+f 474//562 490//578 489//577 473//561
+f 475//563 491//579 490//578 474//562
+f 476//564 492//580 491//579 475//563
+f 477//565 493//581 492//580 476//564
+f 478//566 494//582 493//581 477//565
+f 479//567 495//583 494//582 478//566
+f 480//568 496//584 495//583 479//567
+f 481//569 497//585 512//600 496//584
+f 482//570 498//586 497//585 481//569
+f 483//571 499//587 498//586 482//570
+f 484//572 500//588 499//587 483//571
+f 485//573 501//589 500//588 484//572
+f 486//574 502//590 501//589 485//573
+f 487//575 503//591 502//590 486//574
+f 488//576 504//592 503//591 487//575
+f 489//577 505//593 504//592 488//576
+f 490//578 506//594 505//593 489//577
+f 491//579 507//595 506//594 490//578
+f 492//580 508//596 507//595 491//579
+f 493//581 509//597 508//596 492//580
+f 494//582 510//598 509//597 493//581
+f 495//583 511//599 510//598 494//582
+f 496//584 512//600 511//599 495//583
+f 497//585 513//601 528//616 512//600
+f 498//586 514//602 513//601 497//585
+f 499//587 515//603 514//602 498//586
+f 500//588 516//604 515//603 499//587
+f 501//589 517//605 516//604 500//588
+f 502//590 518//606 517//605 501//589
+f 503//591 519//607 518//606 502//590
+f 504//592 520//608 519//607 503//591
+f 505//593 521//609 520//608 504//592
+f 506//594 522//610 521//609 505//593
+f 507//595 523//611 522//610 506//594
+f 508//596 524//612 523//611 507//595
+f 509//597 525//613 524//612 508//596
+f 510//598 526//614 525//613 509//597
+f 511//599 527//615 526//614 510//598
+f 512//600 528//616 527//615 511//599
+f 513//601 530//618 528//616
+f 514//602 530//618 513//601
+f 515//603 530//618 514//602
+f 516//604 530//618 515//603
+f 517//605 530//618 516//604
+f 518//606 530//618 517//605
+f 519//607 530//618 518//606
+f 520//608 530//618 519//607
+f 521//609 530//618 520//608
+f 522//610 530//618 521//609
+f 523//611 530//618 522//610
+f 524//612 530//618 523//611
+f 525//613 530//618 524//612
+f 526//614 530//618 525//613
+f 527//615 530//618 526//614
+f 528//616 530//618 527//615
+o sphere1
+#114 vertices, 128 faces
+v 0.28854331 2.82104961 0.96000000
+v 0.26657926 2.82104961 1.07042074
+v 0.20403093 2.82104961 1.16403093
+v 0.11042074 2.82104961 1.22657926
+v -1.7491091e-16 2.82104961 1.24854331
+v -0.11042074 2.82104961 1.22657926
+v -0.20403093 2.82104961 1.16403093
+v -0.26657926 2.82104961 1.07042074
+v -0.28854331 2.82104961 0.96000000
+v -0.26657926 2.82104961 0.84957926
+v -0.20403093 2.82104961 0.75596907
+v -0.11042074 2.82104961 0.69342074
+v -2.4558130e-16 2.82104961 0.67145669
+v 0.11042074 2.82104961 0.69342074
+v 0.20403093 2.82104961 0.75596907
+v 0.26657926 2.82104961 0.84957926
+v 0.53315851 2.65760296 0.96000000
+v 0.49257424 2.65760296 1.16403093
+v 0.37700000 2.65760296 1.33700000
+v 0.20403093 2.65760296 1.45257424
+v -1.5993304e-16 2.65760296 1.49315851
+v -0.20403093 2.65760296 1.45257424
+v -0.37700000 2.65760296 1.33700000
+v -0.49257424 2.65760296 1.16403093
+v -0.53315851 2.65760296 0.96000000
+v -0.49257424 2.65760296 0.75596907
+v -0.37700000 2.65760296 0.58300000
+v -0.20403093 2.65760296 0.46742576
+v -2.9051490e-16 2.65760296 0.42684149
+v 0.20403093 2.65760296 0.46742576
+v 0.37700000 2.65760296 0.58300000
+v 0.49257424 2.65760296 0.75596907
+v 0.69660517 2.41298775 0.96000000
+v 0.64357926 2.41298775 1.22657926
+v 0.49257424 2.41298775 1.45257424
+v 0.26657926 2.41298775 1.60357926
+v -1.4992515e-16 2.41298775 1.65660517
+v -0.26657926 2.41298775 1.60357926
+v -0.49257424 2.41298775 1.45257424
+v -0.64357926 2.41298775 1.22657926
+v -0.69660517 2.41298775 0.96000000
+v -0.64357926 2.41298775 0.69342074
+v -0.49257424 2.41298775 0.46742576
+v -0.26657926 2.41298775 0.31642074
+v -3.2053857e-16 2.41298775 0.26339483
+v 0.26657926 2.41298775 0.31642074
+v 0.49257424 2.41298775 0.46742576
+v 0.64357926 2.41298775 0.69342074
+v 0.75400000 2.12444444 0.96000000
+v 0.69660517 2.12444444 1.24854331
+v 0.53315851 2.12444444 1.49315851
+v 0.28854331 2.12444444 1.65660517
+v -1.4641085e-16 2.12444444 1.71400000
+v -0.28854331 2.12444444 1.65660517
+v -0.53315851 2.12444444 1.49315851
+v -0.69660517 2.12444444 1.24854331
+v -0.75400000 2.12444444 0.96000000
+v -0.69660517 2.12444444 0.67145669
+v -0.53315851 2.12444444 0.42684149
+v -0.28854331 2.12444444 0.26339483
+v -3.3108149e-16 2.12444444 0.20600000
+v 0.28854331 2.12444444 0.26339483
+v 0.53315851 2.12444444 0.42684149
+v 0.69660517 2.12444444 0.67145669
+v 0.69660517 1.83590114 0.96000000
+v 0.64357926 1.83590114 1.22657926
+v 0.49257424 1.83590114 1.45257424
+v 0.26657926 1.83590114 1.60357926
+v -1.4992515e-16 1.83590114 1.65660517
+v -0.26657926 1.83590114 1.60357926
+v -0.49257424 1.83590114 1.45257424
+v -0.64357926 1.83590114 1.22657926
+v -0.69660517 1.83590114 0.96000000
+v -0.64357926 1.83590114 0.69342074
+v -0.49257424 1.83590114 0.46742576
+v -0.26657926 1.83590114 0.31642074
+v -3.2053857e-16 1.83590114 0.26339483
+v 0.26657926 1.83590114 0.31642074
+v 0.49257424 1.83590114 0.46742576
+v 0.64357926 1.83590114 0.69342074
+v 0.53315851 1.59128593 0.96000000
+v 0.49257424 1.59128593 1.16403093
+v 0.37700000 1.59128593 1.33700000
+v 0.20403093 1.59128593 1.45257424
+v -1.5993304e-16 1.59128593 1.49315851
+v -0.20403093 1.59128593 1.45257424
+v -0.37700000 1.59128593 1.33700000
+v -0.49257424 1.59128593 1.16403093
+v -0.53315851 1.59128593 0.96000000
+v -0.49257424 1.59128593 0.75596907
+v -0.37700000 1.59128593 0.58300000
+v -0.20403093 1.59128593 0.46742576
+v -2.9051490e-16 1.59128593 0.42684149
+v 0.20403093 1.59128593 0.46742576
+v 0.37700000 1.59128593 0.58300000
+v 0.49257424 1.59128593 0.75596907
+v 0.28854331 1.42783928 0.96000000
+v 0.26657926 1.42783928 1.07042074
+v 0.20403093 1.42783928 1.16403093
+v 0.11042074 1.42783928 1.22657926
+v -1.7491091e-16 1.42783928 1.24854331
+v -0.11042074 1.42783928 1.22657926
+v -0.20403093 1.42783928 1.16403093
+v -0.26657926 1.42783928 1.07042074
+v -0.28854331 1.42783928 0.96000000
+v -0.26657926 1.42783928 0.84957926
+v -0.20403093 1.42783928 0.75596907
+v -0.11042074 1.42783928 0.69342074
+v -2.4558130e-16 1.42783928 0.67145669
+v 0.11042074 1.42783928 0.69342074
+v 0.20403093 1.42783928 0.75596907
+v 0.26657926 1.42783928 0.84957926
+v -1.9257851e-16 2.87844444 0.96000000
+v -1.9257851e-16 1.37044444 0.96000000
+vn 0.38219484 0.92408176 -8.9092210e-16
+vn 0.35310199 0.92408176 0.14625963
+vn 0.27025256 0.92408176 0.27025256
+vn 0.14625963 0.92408176 0.35310199
+vn 7.0989809e-18 0.92408176 0.38219484
+vn -0.14625963 0.92408176 0.35310199
+vn -0.27025256 0.92408176 0.27025256
+vn -0.35310199 0.92408176 0.14625963
+vn -0.38219484 0.92408176 7.0989809e-17
+vn -0.35310199 0.92408176 -0.14625963
+vn -0.27025256 0.92408176 -0.27025256
+vn -0.14625963 0.92408176 -0.35310199
+vn 3.5494904e-18 0.92408176 -0.38219484
+vn 0.14625963 0.92408176 -0.35310199
+vn 0.27025256 0.92408176 -0.27025256
+vn 0.35310199 0.92408176 -0.14625963
+vn 0.70658450 0.70762868 8.5731882e-17
+vn 0.65279895 0.70762868 0.27039818
+vn 0.49963069 0.70762868 0.49963069
+vn 0.27039818 0.70762868 0.65279895
+vn -9.2876205e-17 0.70762868 0.70658450
+vn -0.27039818 0.70762868 0.65279895
+vn -0.49963069 0.70762868 0.49963069
+vn -0.65279895 0.70762868 0.27039818
+vn -0.70658450 0.70762868 1.5003079e-16
+vn -0.65279895 0.70762868 -0.27039818
+vn -0.49963069 0.70762868 -0.49963069
+vn -0.27039818 0.70762868 -0.65279895
+vn -7.1443235e-18 0.70762868 -0.70658450
+vn 0.27039818 0.70762868 -0.65279895
+vn 0.49963069 0.70762868 -0.49963069
+vn 0.65279895 0.70762868 -0.27039818
+vn 0.92368212 0.38315969 -4.3137173e-17
+vn 0.85337100 0.38315969 0.35347784
+vn 0.65314189 0.38315969 0.65314189
+vn 0.35347784 0.38315969 0.85337100
+vn -1.1503246e-16 0.38315969 0.92368212
+vn -0.35347784 0.38315969 0.85337100
+vn -0.65314189 0.38315969 0.65314189
+vn -0.85337100 0.38315969 0.35347784
+vn -0.92368212 0.38315969 2.0849634e-16
+vn -0.85337100 0.38315969 -0.35347784
+vn -0.65314189 0.38315969 -0.65314189
+vn -0.35347784 0.38315969 -0.85337100
+vn -3.5947644e-17 0.38315969 -0.92368212
+vn 0.35347784 0.38315969 -0.85337100
+vn 0.65314189 0.38315969 -0.65314189
+vn 0.85337100 0.38315969 -0.35347784
+vn 1.00000000 5.7665700e-17 -1.6578889e-16
+vn 0.92387953 7.2082126e-18 0.38268343
+vn 0.70710678 -1.4416425e-17 0.70710678
+vn 0.38268343 2.1624638e-17 0.92387953
+vn -2.1624638e-17 -7.2082126e-18 1.00000000
+vn -0.38268343 1.4416425e-17 0.92387953
+vn -0.70710678 1.1533140e-16 0.70710678
+vn -0.92387953 1.5137246e-16 0.38268343
+vn -1.00000000 8.6498551e-17 2.5228744e-16
+vn -0.92387953 -7.2082126e-18 -0.38268343
+vn -0.70710678 -5.0457488e-17 -0.70710678
+vn -0.38268343 2.8832850e-17 -0.92387953
+vn -6.4873913e-17 5.0457488e-17 -1.00000000
+vn 0.38268343 5.7665700e-17 -0.92387953
+vn 0.70710678 -1.4416425e-17 -0.70710678
+vn 0.92387953 -2.1624638e-17 -0.38268343
+vn 0.92368212 -0.38315969 -1.1503246e-16
+vn 0.85337100 -0.38315969 0.35347784
+vn 0.65314189 -0.38315969 0.65314189
+vn 0.35347784 -0.38315969 0.85337100
+vn -2.1568586e-17 -0.38315969 0.92368212
+vn -0.35347784 -0.38315969 0.85337100
+vn -0.65314189 -0.38315969 0.65314189
+vn -0.85337100 -0.38315969 0.35347784
+vn -0.92368212 -0.38315969 2.3006492e-16
+vn -0.85337100 -0.38315969 -0.35347784
+vn -0.65314189 -0.38315969 -0.65314189
+vn -0.35347784 -0.38315969 -0.85337100
+vn -4.3137173e-17 -0.38315969 -0.92368212
+vn 0.35347784 -0.38315969 -0.85337100
+vn 0.65314189 -0.38315969 -0.65314189
+vn 0.85337100 -0.38315969 -0.35347784
+vn 0.70658450 -0.70762868 8.2159720e-17
+vn 0.65279895 -0.70762868 0.27039818
+vn 0.49963069 -0.70762868 0.49963069
+vn 0.27039818 -0.70762868 0.65279895
+vn -1.4288647e-17 -0.70762868 0.70658450
+vn -0.27039818 -0.70762868 0.65279895
+vn -0.49963069 -0.70762868 0.49963069
+vn -0.65279895 -0.70762868 0.27039818
+vn -0.70658450 -0.70762868 2.1432970e-16
+vn -0.65279895 -0.70762868 -0.27039818
+vn -0.49963069 -0.70762868 -0.49963069
+vn -0.27039818 -0.70762868 -0.65279895
+vn -2.5005132e-17 -0.70762868 -0.70658450
+vn 0.27039818 -0.70762868 -0.65279895
+vn 0.49963069 -0.70762868 -0.49963069
+vn 0.65279895 -0.70762868 -0.27039818
+vn 0.38219484 -0.92408176 -1.7747452e-16
+vn 0.35310199 -0.92408176 0.14625963
+vn 0.27025256 -0.92408176 0.27025256
+vn 0.14625963 -0.92408176 0.35310199
+vn 0.0000000e+0 -0.92408176 0.38219484
+vn -0.14625963 -0.92408176 0.35310199
+vn -0.27025256 -0.92408176 0.27025256
+vn -0.35310199 -0.92408176 0.14625963
+vn -0.38219484 -0.92408176 5.3952254e-16
+vn -0.35310199 -0.92408176 -0.14625963
+vn -0.27025256 -0.92408176 -0.27025256
+vn -0.14625963 -0.92408176 -0.35310199
+vn -1.7747452e-17 -0.92408176 -0.38219484
+vn 0.14625963 -0.92408176 -0.35310199
+vn 0.27025256 -0.92408176 -0.27025256
+vn 0.35310199 -0.92408176 -0.14625963
+vn -1.5930360e-17 1.00000000 8.7616978e-17
+vn -4.6021039e-17 -1.00000000 2.6550599e-17
+g sphere1_default
+usemtl default
+f 531//619 547//635 562//650 546//634
+f 531//619 643//731 532//620
+f 532//620 548//636 547//635 531//619
+f 532//620 643//731 533//621
+f 533//621 549//637 548//636 532//620
+f 533//621 643//731 534//622
+f 534//622 550//638 549//637 533//621
+f 534//622 643//731 535//623
+f 535//623 551//639 550//638 534//622
+f 535//623 643//731 536//624
+f 536//624 552//640 551//639 535//623
+f 536//624 643//731 537//625
+f 537//625 553//641 552//640 536//624
+f 537//625 643//731 538//626
+f 538//626 554//642 553//641 537//625
+f 538//626 643//731 539//627
+f 539//627 555//643 554//642 538//626
+f 539//627 643//731 540//628
+f 540//628 556//644 555//643 539//627
+f 540//628 643//731 541//629
+f 541//629 557//645 556//644 540//628
+f 541//629 643//731 542//630
+f 542//630 558//646 557//645 541//629
+f 542//630 643//731 543//631
+f 543//631 559//647 558//646 542//630
+f 543//631 643//731 544//632
+f 544//632 560//648 559//647 543//631
+f 544//632 643//731 545//633
+f 545//633 561//649 560//648 544//632
+f 545//633 643//731 546//634
+f 546//634 562//650 561//649 545//633
+f 546//634 643//731 531//619
+f 547//635 563//651 578//666 562//650
+f 548//636 564//652 563//651 547//635
+f 549//637 565//653 564//652 548//636
+f 550//638 566//654 565//653 549//637
+f 551//639 567//655 566//654 550//638
+f 552//640 568//656 567//655 551//639
+f 553//641 569//657 568//656 552//640
+f 554//642 570//658 569//657 553//641
+f 555//643 571//659 570//658 554//642
+f 556//644 572//660 571//659 555//643
+f 557//645 573//661 572//660 556//644
+f 558//646 574//662 573//661 557//645
+f 559//647 575//663 574//662 558//646
+f 560//648 576//664 575//663 559//647
+f 561//649 577//665 576//664 560//648
+f 562//650 578//666 577//665 561//649
+f 563//651 579//667 594//682 578//666
+f 564//652 580//668 579//667 563//651
+f 565//653 581//669 580//668 564//652
+f 566//654 582//670 581//669 565//653
+f 567//655 583//671 582//670 566//654
+f 568//656 584//672 583//671 567//655
+f 569//657 585//673 584//672 568//656
+f 570//658 586//674 585//673 569//657
+f 571//659 587//675 586//674 570//658
+f 572//660 588//676 587//675 571//659
+f 573//661 589//677 588//676 572//660
+f 574//662 590//678 589//677 573//661
+f 575//663 591//679 590//678 574//662
+f 576//664 592//680 591//679 575//663
+f 577//665 593//681 592//680 576//664
+f 578//666 594//682 593//681 577//665
+f 579//667 595//683 610//698 594//682
+f 580//668 596//684 595//683 579//667
+f 581//669 597//685 596//684 580//668
+f 582//670 598//686 597//685 581//669
+f 583//671 599//687 598//686 582//670
+f 584//672 600//688 599//687 583//671
+f 585//673 601//689 600//688 584//672
+f 586//674 602//690 601//689 585//673
+f 587//675 603//691 602//690 586//674
+f 588//676 604//692 603//691 587//675
+f 589//677 605//693 604//692 588//676
+f 590//678 606//694 605//693 589//677
+f 591//679 607//695 606//694 590//678
+f 592//680 608//696 607//695 591//679
+f 593//681 609//697 608//696 592//680
+f 594//682 610//698 609//697 593//681
+f 595//683 611//699 626//714 610//698
+f 596//684 612//700 611//699 595//683
+f 597//685 613//701 612//700 596//684
+f 598//686 614//702 613//701 597//685
+f 599//687 615//703 614//702 598//686
+f 600//688 616//704 615//703 599//687
+f 601//689 617//705 616//704 600//688
+f 602//690 618//706 617//705 601//689
+f 603//691 619//707 618//706 602//690
+f 604//692 620//708 619//707 603//691
+f 605//693 621//709 620//708 604//692
+f 606//694 622//710 621//709 605//693
+f 607//695 623//711 622//710 606//694
+f 608//696 624//712 623//711 607//695
+f 609//697 625//713 624//712 608//696
+f 610//698 626//714 625//713 609//697
+f 611//699 627//715 642//730 626//714
+f 612//700 628//716 627//715 611//699
+f 613//701 629//717 628//716 612//700
+f 614//702 630//718 629//717 613//701
+f 615//703 631//719 630//718 614//702
+f 616//704 632//720 631//719 615//703
+f 617//705 633//721 632//720 616//704
+f 618//706 634//722 633//721 617//705
+f 619//707 635//723 634//722 618//706
+f 620//708 636//724 635//723 619//707
+f 621//709 637//725 636//724 620//708
+f 622//710 638//726 637//725 621//709
+f 623//711 639//727 638//726 622//710
+f 624//712 640//728 639//727 623//711
+f 625//713 641//729 640//728 624//712
+f 626//714 642//730 641//729 625//713
+f 627//715 644//732 642//730
+f 628//716 644//732 627//715
+f 629//717 644//732 628//716
+f 630//718 644//732 629//717
+f 631//719 644//732 630//718
+f 632//720 644//732 631//719
+f 633//721 644//732 632//720
+f 634//722 644//732 633//721
+f 635//723 644//732 634//722
+f 636//724 644//732 635//723
+f 637//725 644//732 636//724
+f 638//726 644//732 637//725
+f 639//727 644//732 638//726
+f 640//728 644//732 639//727
+f 641//729 644//732 640//728
+f 642//730 644//732 641//729
diff --git a/contrib/model/examples/tree_test.py b/contrib/model/examples/tree_test.py
new file mode 100644
index 0000000..76230d9
--- /dev/null
+++ b/contrib/model/examples/tree_test.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+
+'''
+'''
+
+__docformat__ = 'restructuredtext'
+__version__ = '$Id: gl_tree_test.py 114 2006-10-22 09:46:11Z r1chardj0n3s $'
+
+import math
+import random
+from pyglet.window import *
+from pyglet.window import mouse
+from pyglet import clock
+from ctypes import *
+from pyglet.window.event import *
+# from pyglet.ext.model.geometric import tree_list
+from model.geometric import tree_list
+
+from pyglet.gl import *
+
+four_floats = c_float * 4
+
+def setup_scene():
+    glEnable(GL_DEPTH_TEST)
+    glMatrixMode(GL_PROJECTION)
+    glLoadIdentity()
+    gluPerspective(60., 1., 1., 100.)
+    glMatrixMode(GL_MODELVIEW)
+    glClearColor(1, 1, 1, 1)
+
+class Tree:
+    def __init__(self, n=2, r=False):
+        self.tree = tree_list(n, r)
+        self.x = self.y = 0
+        self.rx = self.ry = 0
+        self.zpos = -10
+        self.lmb = False
+        self.rmb = False
+
+    def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
+        if buttons & mouse.LEFT:
+            self.rx += dx
+            self.ry += dy
+        if buttons & mouse.RIGHT:
+            self.x += dx
+            self.y += dy
+
+    def on_mouse_scroll(self, x, y, dx, dy):
+        self.zpos = max(-10, self.zpos + dy)
+
+    def draw(self):
+        glPushMatrix()
+        glLoadIdentity()
+        glTranslatef(self.x/10., -self.y/10., self.zpos)
+        glRotatef(self.ry, 1., 0., 0.)
+        glRotatef(self.rx, 0., 1., 0.)
+        self.tree.draw()
+        glPopMatrix()
+
+# need one display list per window
+w1 = Window(width=300, height=300)
+w1.switch_to()
+setup_scene()
+tree1 = Tree(n=10, r=True)
+w1.push_handlers(tree1)
+
+w2 = Window(width=300, height=300)
+w2.switch_to()
+setup_scene()
+tree2 = Tree(n=10, r=False)
+w2.push_handlers(tree2)
+
+n = 0
+clock.set_fps_limit(30)
+while not (w1.has_exit or w2.has_exit):
+    clock.tick()
+    n += 1
+
+    # draw
+    w1.switch_to()
+    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
+    w1.dispatch_events()
+    tree1.draw()
+    w1.flip()
+
+    w2.switch_to()
+    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
+    w2.dispatch_events()
+    tree2.draw()
+    w2.flip()
+
diff --git a/contrib/model/model/__init__.py b/contrib/model/model/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/contrib/model/model/geometric.py b/contrib/model/model/geometric.py
new file mode 100644
index 0000000..45b358b
--- /dev/null
+++ b/contrib/model/model/geometric.py
@@ -0,0 +1,192 @@
+import math
+import random
+
+from pyglet.gl import *
+import euclid
+
+M_2PI = math.pi * 2.0
+
+
+class DisplayList(int):
+    def draw(self):
+        glCallList(self)
+
+
+def _TRI(first, second, third):
+    (v1, n1, t1, b1) = first
+    (v2, n2, t2, b2) = second
+    (v3, n3, t3, b3) = third
+    glNormal3f(n1[0], n1[1], n1[2])
+    glVertex3f(v1[0], v1[1], v1[2])
+    glNormal3f(n2[0], n2[1], n2[2])
+    glVertex3f(v2[0], v2[1], v2[2])
+    glNormal3f(n3[0], n3[1], n3[2])
+    glVertex3f(v3[0], v3[1], v3[2])
+
+
+def render_torus(NR=40, NS=40, rad1=5.0, rad2=1.5):
+    def P(R, S):
+        R = R * M_2PI / NR
+        S = S * M_2PI / NS
+        y = rad2 * math.sin(S)
+        r = rad1 + rad2 * math.cos(S)
+        x = r * math.cos(R)
+        z = r * math.sin(R)
+
+        nx = math.cos(R) * math.cos(S)
+        nz = math.sin(R) * math.cos(S)
+        ny = math.sin(S)
+
+        tx = math.cos(R) * -math.sin(S)
+        tz = math.sin(R) * -math.sin(S)
+        ty = math.cos(S)
+
+        bx = ny * tz - nz * ty
+        by = nz * tx - nx * tz
+        bz = nx * ty - ny * tx
+
+        return (x, y, z), (nx, ny, nz), (tx, tz, ty), (bx, by, bz)
+
+    glBegin(GL_TRIANGLES)
+    for ring in range(NR):
+        for stripe in range(NR):
+            _TRI(P(ring, stripe), P(ring, stripe + 1),
+                 P(ring + 1, stripe + 1))
+            _TRI(P(ring, stripe), P(ring + 1, stripe + 1),
+                 P(ring + 1, stripe))
+    glEnd()
+
+
+def torus_list():
+    torus_dl = glGenLists(1)
+    glNewList(torus_dl, GL_COMPILE)
+    render_torus()
+    glEndList()
+    return DisplayList(torus_dl)
+
+
+def render_cube():
+    glBegin(GL_TRIANGLES)
+    glNormal3f(+1.0, 0.0, 0.0)
+    glVertex3f(+1.0, +1.0, +1.0)
+    glVertex3f(+1.0, -1.0, +1.0)
+    glVertex3f(+1.0, -1.0, -1.0)
+    glVertex3f(+1.0, +1.0, +1.0)
+    glVertex3f(+1.0, -1.0, -1.0)
+    glVertex3f(+1.0, +1.0, -1.0)
+
+    glNormal3f(0.0, +1.0, 0.0)
+    glVertex3f(+1.0, +1.0, +1.0)
+    glVertex3f(-1.0, +1.0, +1.0)
+    glVertex3f(-1.0, +1.0, -1.0)
+    glVertex3f(+1.0, +1.0, +1.0)
+    glVertex3f(-1.0, +1.0, -1.0)
+    glVertex3f(+1.0, +1.0, -1.0)
+
+    glNormal3f(0.0, 0.0, +1.0)
+    glVertex3f(+1.0, +1.0, +1.0)
+    glVertex3f(-1.0, +1.0, +1.0)
+    glVertex3f(-1.0, -1.0, +1.0)
+    glVertex3f(+1.0, +1.0, +1.0)
+    glVertex3f(-1.0, -1.0, +1.0)
+    glVertex3f(+1.0, -1.0, +1.0)
+
+    glNormal3f(-1.0, 0.0, 0.0)
+    glVertex3f(-1.0, -1.0, -1.0)
+    glVertex3f(-1.0, -1.0, +1.0)
+    glVertex3f(-1.0, +1.0, +1.0)
+    glVertex3f(-1.0, +1.0, -1.0)
+    glVertex3f(-1.0, -1.0, -1.0)
+    glVertex3f(-1.0, +1.0, +1.0)
+
+    glNormal3f(0.0, -1.0, 0.0)
+    glVertex3f(-1.0, -1.0, -1.0)
+    glVertex3f(-1.0, -1.0, +1.0)
+    glVertex3f(+1.0, -1.0, +1.0)
+    glVertex3f(+1.0, -1.0, -1.0)
+    glVertex3f(-1.0, -1.0, -1.0)
+    glVertex3f(+1.0, -1.0, +1.0)
+
+    glNormal3f(0.0, 0.0, -1.0)
+    glVertex3f(-1.0, -1.0, -1.0)
+    glVertex3f(-1.0, +1.0, -1.0)
+    glVertex3f(+1.0, +1.0, -1.0)
+    glVertex3f(+1.0, -1.0, -1.0)
+    glVertex3f(-1.0, -1.0, -1.0)
+    glVertex3f(+1.0, +1.0, -1.0)
+    glEnd()
+
+
+def cube_list():
+    cube_dl = glGenLists(1)
+    glNewList(cube_dl, GL_COMPILE)
+    render_cube()
+    glEndList()
+    return DisplayList(cube_dl)
+
+
+def cube_array_list():
+    cubes_dl = glGenLists(1)
+    glNewList(cubes_dl, GL_COMPILE)
+    glMatrixMode(GL_MODELVIEW)
+    glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
+    glEnable(GL_COLOR_MATERIAL)
+    glPushMatrix()
+    for x in range(-10, +11, +2):
+        for y in range(-10, +11, +2):
+            for z in range(-10, +11, +2):
+                glPushMatrix()
+                glTranslatef(x * 2.0, y * 2.0, z * 2.0)
+                glScalef(.8, .8, .8)
+                glColor4f((x + 10.0) / 20.0, (y + 10.0) / 20.0,
+                          (z + 10.0) / 20.0, 1.0)
+                render_cube()
+                glPopMatrix()
+    glPopMatrix()
+    glDisable(GL_COLOR_MATERIAL)
+    glEndList()
+    return DisplayList(cubes_dl)
+
+
+_ROT_30_X = euclid.Matrix4.new_rotatex(math.pi / 12)
+_ROT_N30_X = euclid.Matrix4.new_rotatex(-math.pi / 12)
+_ROT_30_Z = euclid.Matrix4.new_rotatez(math.pi / 12)
+_ROT_N30_Z = euclid.Matrix4.new_rotatez(-math.pi / 12)
+
+
+def _tree_branch(n, l, r):
+    glVertex3f(l.p1.x, l.p1.y, l.p1.z)
+    glVertex3f(l.p2.x, l.p2.y, l.p2.z)
+    if n == 0:
+        return
+
+    if r:
+        if random.random() > .9: return
+        mag = abs(l.v) * (.5 + .5 * random.random())
+    else:
+        mag = abs(l.v) * .75
+    if n % 2:
+        v1 = _ROT_30_X * l.v
+        v2 = _ROT_N30_X * l.v
+    else:
+        v1 = _ROT_30_Z * l.v
+        v2 = _ROT_N30_Z * l.v
+    _tree_branch(n - 1, euclid.Line3(l.p2, v1, mag), r)
+    _tree_branch(n - 1, euclid.Line3(l.p2, v2, mag), r)
+
+
+def render_tree(n=10, r=False):
+    glLineWidth(2.)
+    glColor4f(.5, .5, .5, .5)
+    glBegin(GL_LINES)
+    _tree_branch(n - 1, euclid.Line3(euclid.Point3(0., 0., 0.),
+                                     euclid.Vector3(0., 1., 0.), 1.), r)
+    glEnd()
+
+
+def tree_list(n=10, r=False):
+    dl = glGenLists(1)
+    glNewList(dl, GL_COMPILE)
+    render_tree(n, r)
+    glEndList()
+    return DisplayList(dl)
diff --git a/contrib/model/model/obj.py b/contrib/model/model/obj.py
new file mode 100644
index 0000000..0152192
--- /dev/null
+++ b/contrib/model/model/obj.py
@@ -0,0 +1,210 @@
+import os
+import warnings
+
+from pyglet.gl import *
+from pyglet import image
+
+class Material:
+    diffuse = [.8, .8, .8]
+    ambient = [.2, .2, .2]
+    specular = [0., 0., 0.]
+    emission = [0., 0., 0.]
+    shininess = 0.
+    opacity = 1.
+    texture = None
+
+    def __init__(self, name):
+        self.name = name
+
+    def apply(self, face=GL_FRONT_AND_BACK):
+        if self.texture:
+            glEnable(self.texture.target)
+            glBindTexture(self.texture.target, self.texture.id)
+        else:
+            glDisable(GL_TEXTURE_2D)
+
+        glMaterialfv(face, GL_DIFFUSE,
+            (GLfloat * 4)(*(self.diffuse + [self.opacity])))
+        glMaterialfv(face, GL_AMBIENT,
+            (GLfloat * 4)(*(self.ambient + [self.opacity])))
+        glMaterialfv(face, GL_SPECULAR,
+            (GLfloat * 4)(*(self.specular + [self.opacity])))
+        glMaterialfv(face, GL_EMISSION,
+            (GLfloat * 4)(*(self.emission + [self.opacity])))
+        glMaterialf(face, GL_SHININESS, self.shininess)
+
+class MaterialGroup:
+    def __init__(self, material):
+        self.material = material
+
+        # Interleaved array of floats in GL_T2F_N3F_V3F format
+        self.vertices = []
+        self.array = None
+
+class Mesh:
+    def __init__(self, name):
+        self.name = name
+        self.groups = []
+
+        # Display list, created only if compile() is called, but used
+        # automatically by draw()
+        self.list = None
+
+    def draw(self):
+        if self.list:
+            glCallList(self.list)
+            return
+
+        glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
+        glPushAttrib(GL_CURRENT_BIT | GL_ENABLE_BIT | GL_LIGHTING_BIT)
+        glEnable(GL_CULL_FACE)
+        glCullFace(GL_BACK)
+        for group in self.groups:
+            group.material.apply()
+            if group.array is None:
+                group.array = (GLfloat * len(group.vertices))(*group.vertices)
+                group.triangles = len(group.vertices) // 8
+            glInterleavedArrays(GL_T2F_N3F_V3F, 0, group.array)
+            glDrawArrays(GL_TRIANGLES, 0, group.triangles)
+        glPopAttrib()
+        glPopClientAttrib()
+
+    def compile(self):
+        if not self.list:
+            list = glGenLists(1)
+            glNewList(list, GL_COMPILE)
+            self.draw()
+            glEndList()
+            self.list = list
+
+class OBJ:
+    def __init__(self, filename, file=None, path=None):
+        self.materials = {}
+        self.meshes = {}        # Name mapping
+        self.mesh_list = []     # Also includes anonymous meshes
+
+        if file is None:
+            file = open(filename, 'r')
+
+        if path is None:
+            path = os.path.dirname(filename)
+        self.path = path
+
+        mesh = None
+        group = None
+        material = None
+
+        vertices = [[0., 0., 0.]]
+        normals = [[0., 0., 0.]]
+        tex_coords = [[0., 0.]]
+
+        for line in open(filename, "r"):
+            if line.startswith('#'): 
+                continue
+            values = line.split()
+            if not values: 
+                continue
+
+            if values[0] == 'v':
+                vertices.append(list(map(float, values[1:4])))
+            elif values[0] == 'vn':
+                normals.append(list(map(float, values[1:4])))
+            elif values[0] == 'vt':
+                tex_coords.append(list(map(float, values[1:3])))
+            elif values[0] == 'mtllib':
+                self.load_material_library(values[1])
+            elif values[0] in ('usemtl', 'usemat'):
+                material = self.materials.get(values[1], None)
+                if material is None:
+                    warnings.warn('Unknown material: %s' % values[1])
+                if mesh is not None:
+                    group = MaterialGroup(material)
+                    mesh.groups.append(group)
+            elif values[0] == 'o':
+                mesh = Mesh(values[1])
+                self.meshes[mesh.name] = mesh
+                self.mesh_list.append(mesh)
+                group = None
+            elif values[0] == 'f':
+                if mesh is None:
+                    mesh = Mesh('')
+                    self.mesh_list.append(mesh)
+                if material is None:
+                    material = Material()
+                if group is None:
+                    group = MaterialGroup(material)
+                    mesh.groups.append(group)
+
+                # For fan triangulation, remember first and latest vertices
+                v1 = None
+                vlast = None
+                points = []
+                for i, v in enumerate(values[1:]):
+                    v_index, t_index, n_index = \
+                        (list(map(int, [j or 0 for j in v.split('/')])) + [0, 0])[:3]
+                    if v_index < 0:
+                        v_index += len(vertices) - 1
+                    if t_index < 0:
+                        t_index += len(tex_coords) - 1
+                    if n_index < 0:
+                        n_index += len(normals) - 1
+                    vertex = tex_coords[t_index] + \
+                             normals[n_index] + \
+                             vertices[v_index] 
+
+                    if i >= 3:
+                        # Triangulate
+                        group.vertices += v1 + vlast
+                    group.vertices += vertex
+
+                    if i == 0:
+                        v1 = vertex
+                    vlast = vertex
+                    
+    def open_material_file(self, filename):
+        '''Override for loading from archive/network etc.'''
+        return open(os.path.join(self.path, filename), 'r')
+
+    def load_material_library(self, filename):
+        material = None
+        file = self.open_material_file(filename)
+
+        for line in file:
+            if line.startswith('#'):
+                continue
+            values = line.split()
+            if not values:
+                continue
+
+            if values[0] == 'newmtl':
+                material = Material(values[1])
+                self.materials[material.name] = material
+            elif material is None:
+                warnings.warn('Expected "newmtl" in %s' % filename)
+                continue
+
+            try:
+                if values[0] == 'Kd':
+                    material.diffuse = list(map(float, values[1:]))
+                elif values[0] == 'Ka':
+                    material.ambient = list(map(float, values[1:]))
+                elif values[0] == 'Ks':
+                    material.specular = list(map(float, values[1:]))
+                elif values[0] == 'Ke':
+                    material.emissive = list(map(float, values[1:]))
+                elif values[0] == 'Ns':
+                    material.shininess = float(values[1])
+                elif values[0] == 'd':
+                    material.opacity = float(values[1])
+                elif values[0] == 'map_Kd':
+                    try:
+                        material.texture = image.load(values[1]).texture
+                    except image.ImageDecodeException:
+                        warnings.warn('Could not load texture %s' % values[1])
+            except:
+                warnings.warn('Parse error in %s.' % filename)
+
+    def draw(self):
+        for mesh in self.mesh_list:
+            mesh.draw()
+
diff --git a/contrib/model/model/obj_batch.py b/contrib/model/model/obj_batch.py
new file mode 100644
index 0000000..3d21e49
--- /dev/null
+++ b/contrib/model/model/obj_batch.py
@@ -0,0 +1,309 @@
+"""
+Wavefront OBJ renderer using pyglet's Batch class.
+
+Based on obj.py but this should be more efficient and
+uses VBOs when available instead of the deprecated
+display lists.
+
+First load the object:
+
+    obj = OBJ("object.obj")
+
+Alternatively you can use `OBJ.from_resource(filename)`
+to load the object, materials and textures using
+pyglet's resource framework.
+
+After the object is loaded, add it to a Batch:
+
+    obj.add_to(batch)
+
+Then you only have to draw your batch!
+
+Juan J. Martinez <jjm@usebox.net>
+"""
+import os
+import logging
+
+from pyglet.gl import *
+from pyglet import image, resource, graphics
+
+
+class Material(graphics.Group):
+    diffuse = [.8, .8, .8]
+    ambient = [.2, .2, .2]
+    specular = [0., 0., 0.]
+    emission = [0., 0., 0.]
+    shininess = 0.
+    opacity = 1.
+    texture = None
+
+    def __init__(self, name, **kwargs):
+        self.name = name
+        super(Material, self).__init__(**kwargs)
+
+    def set_state(self, face=GL_FRONT_AND_BACK):
+        if self.texture:
+            glEnable(self.texture.target)
+            glBindTexture(self.texture.target, self.texture.id)
+        else:
+            glDisable(GL_TEXTURE_2D)
+
+        glMaterialfv(face, GL_DIFFUSE,
+                     (GLfloat * 4)(*(self.diffuse + [self.opacity])))
+        glMaterialfv(face, GL_AMBIENT,
+                     (GLfloat * 4)(*(self.ambient + [self.opacity])))
+        glMaterialfv(face, GL_SPECULAR,
+                     (GLfloat * 4)(*(self.specular + [self.opacity])))
+        glMaterialfv(face, GL_EMISSION,
+                     (GLfloat * 4)(*(self.emission + [self.opacity])))
+        glMaterialf(face, GL_SHININESS, self.shininess)
+
+    def unset_state(self):
+        if self.texture:
+            glDisable(self.texture.target)
+        glDisable(GL_COLOR_MATERIAL)
+
+    def __eq__(self, other):
+        if self.texture is None:
+            return super(Material, self).__eq__(other)
+        return (self.__class__ is other.__class__ and
+                self.texture.id == other.texture.id and
+                self.texture.target == other.texture.target and
+                self.parent == other.parent)
+
+    def __hash__(self):
+        if self.texture is None:
+            return super(Material, self).__hash__()
+        return hash((self.texture.id, self.texture.target))
+
+
+class MaterialGroup:
+    def __init__(self, material):
+        self.material = material
+
+        # Interleaved array of floats in GL_T2F_N3F_V3F format
+        self.vertices = []
+        self.normals = []
+        self.tex_coords = []
+        self.array = None
+
+
+class Mesh:
+    def __init__(self, name):
+        self.name = name
+        self.groups = []
+
+
+class OBJ:
+    @staticmethod
+    def from_resource(filename):
+        '''Load an object using the resource framework'''
+        loc = pyglet.resource.location(filename)
+        return OBJ(filename, file=loc.open(filename), path=loc.path)
+
+    def __init__(self, filename, file=None, path=None):
+        self.materials = {}
+        self.meshes = {}  # Name mapping
+        self.mesh_list = []  # Also includes anonymous meshes
+
+        if file is None:
+            file = open(filename, 'r')
+
+        if path is None:
+            path = os.path.dirname(filename)
+        self.path = path
+
+        mesh = None
+        group = None
+        material = None
+
+        vertices = [[0., 0., 0.]]
+        normals = [[0., 0., 0.]]
+        tex_coords = [[0., 0.]]
+
+        for line in file:
+            if line.startswith('#'):
+                continue
+            values = line.split()
+            if not values:
+                continue
+
+            if values[0] == 'v':
+                vertices.append(list(map(float, values[1:4])))
+            elif values[0] == 'vn':
+                normals.append(list(map(float, values[1:4])))
+            elif values[0] == 'vt':
+                tex_coords.append(list(map(float, values[1:3])))
+            elif values[0] == 'mtllib':
+                self.load_material_library(values[1])
+            elif values[0] in ('usemtl', 'usemat'):
+                material = self.materials.get(values[1], None)
+                if material is None:
+                    logging.warn('Unknown material: %s' % values[1])
+                if mesh is not None:
+                    group = MaterialGroup(material)
+                    mesh.groups.append(group)
+            elif values[0] == 'o':
+                mesh = Mesh(values[1])
+                self.meshes[mesh.name] = mesh
+                self.mesh_list.append(mesh)
+                group = None
+            elif values[0] == 'f':
+                if mesh is None:
+                    mesh = Mesh('')
+                    self.mesh_list.append(mesh)
+                if material is None:
+                    # FIXME
+                    material = Material("<unknown>")
+                if group is None:
+                    group = MaterialGroup(material)
+                    mesh.groups.append(group)
+
+                # For fan triangulation, remember first and latest vertices
+                n1 = None
+                nlast = None
+                t1 = None
+                tlast = None
+                v1 = None
+                vlast = None
+                # points = []
+                for i, v in enumerate(values[1:]):
+                    v_index, t_index, n_index = \
+                        (list(map(int, [j or 0 for j in v.split('/')])) + [0, 0])[:3]
+                    if v_index < 0:
+                        v_index += len(vertices) - 1
+                    if t_index < 0:
+                        t_index += len(tex_coords) - 1
+                    if n_index < 0:
+                        n_index += len(normals) - 1
+                    # vertex = tex_coords[t_index] + \
+                    #         normals[n_index] + \
+                    #         vertices[v_index]
+
+                    group.normals += normals[n_index]
+                    group.tex_coords += tex_coords[t_index]
+                    group.vertices += vertices[v_index]
+
+                    if i >= 3:
+                        # Triangulate
+                        group.normals += n1 + nlast
+                        group.tex_coords += t1 + tlast
+                        group.vertices += v1 + vlast
+
+                    if i == 0:
+                        n1 = normals[n_index]
+                        t1 = tex_coords[t_index]
+                        v1 = vertices[v_index]
+                    nlast = normals[n_index]
+                    tlast = tex_coords[t_index]
+                    vlast = vertices[v_index]
+
+    def add_to(self, batch):
+        '''Add the meshes to a batch'''
+        for mesh in self.mesh_list:
+            for group in mesh.groups:
+                batch.add(len(group.vertices) // 3,
+                          GL_TRIANGLES,
+                          group.material,
+                          ('v3f/static', tuple(group.vertices)),
+                          ('n3f/static', tuple(group.normals)),
+                          ('t2f/static', tuple(group.tex_coords)),
+                          )
+
+    def open_material_file(self, filename):
+        '''Override for loading from archive/network etc.'''
+        return open(os.path.join(self.path, filename), 'r')
+
+    def load_material_library(self, filename):
+        material = None
+        file = self.open_material_file(filename)
+
+        for line in file:
+            if line.startswith('#'):
+                continue
+            values = line.split()
+            if not values:
+                continue
+
+            if values[0] == 'newmtl':
+                material = Material(values[1])
+                self.materials[material.name] = material
+            elif material is None:
+                logging.warn('Expected "newmtl" in %s' % filename)
+                continue
+
+            try:
+                if values[0] == 'Kd':
+                    material.diffuse = list(map(float, values[1:]))
+                elif values[0] == 'Ka':
+                    material.ambient = list(map(float, values[1:]))
+                elif values[0] == 'Ks':
+                    material.specular = list(map(float, values[1:]))
+                elif values[0] == 'Ke':
+                    material.emissive = list(map(float, values[1:]))
+                elif values[0] == 'Ns':
+                    material.shininess = float(values[1])
+                elif values[0] == 'd':
+                    material.opacity = float(values[1])
+                elif values[0] == 'map_Kd':
+                    try:
+                        material.texture = pyglet.resource.image(values[1]).texture
+                    except BaseException as ex:
+                        logging.warn('Could not load texture %s: %s' % (values[1], ex))
+            except BaseException as ex:
+                logging.warning('Parse error in %s.' % (filename, ex))
+
+
+if __name__ == "__main__":
+    import sys
+    import ctypes
+
+    if len(sys.argv) != 2:
+        logging.error("Usage: %s file.obj" % sys.argv[0])
+    else:
+        window = pyglet.window.Window()
+
+        fourfv = ctypes.c_float * 4
+        glLightfv(GL_LIGHT0, GL_POSITION, fourfv(0, 200, 5000, 1))
+        glLightfv(GL_LIGHT0, GL_AMBIENT, fourfv(0.0, 0.0, 0.0, 1.0))
+        glLightfv(GL_LIGHT0, GL_DIFFUSE, fourfv(1.0, 1.0, 1.0, 1.0))
+        glLightfv(GL_LIGHT0, GL_SPECULAR, fourfv(1.0, 1.0, 1.0, 1.0))
+        glEnable(GL_LIGHT0)
+        glEnable(GL_LIGHTING)
+        glEnable(GL_DEPTH_TEST)
+
+
+        @window.event
+        def on_resize(width, height):
+            glMatrixMode(GL_PROJECTION)
+            glLoadIdentity()
+            gluPerspective(60.0, float(width) / height, 1.0, 100.0)
+            glMatrixMode(GL_MODELVIEW)
+            return True
+
+
+        @window.event
+        def on_draw():
+            window.clear()
+            glLoadIdentity()
+            gluLookAt(0, 3, 3, 0, 0, 0, 0, 1, 0)
+            glRotatef(rot, 1, 0, 0)
+            glRotatef(rot / 2, 0, 1, 0)
+            batch.draw()
+
+
+        rot = 0
+
+
+        def update(dt):
+            global rot
+            rot += dt * 75
+
+
+        pyglet.clock.schedule_interval(update, 1.0 / 60)
+
+        obj = OBJ(sys.argv[1])
+        batch = pyglet.graphics.Batch()
+        obj.add_to(batch)
+
+        pyglet.app.run()
diff --git a/contrib/toys/euclid.py b/contrib/toys/euclid.py
new file mode 100644
index 0000000..028a9e4
--- /dev/null
+++ b/contrib/toys/euclid.py
@@ -0,0 +1,2125 @@
+#!/usr/bin/env python
+#
+# euclid graphics maths module
+#
+# Copyright (c) 2006 Alex Holkner
+# Alex.Holkner@mail.google.com
+#
+# This library is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 2.1 of the License, or (at your
+# option) any later version.
+#
+# This library is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
+# for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
+
+'''euclid graphics maths module
+
+Documentation and tests are included in the file "euclid.txt", or online
+at http://code.google.com/p/pyeuclid
+'''
+
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+__revision__ = '$Revision$'
+
+import math
+import operator
+import types
+
+try:
+    import itertools.imap as map  # XXX  Python3 has changed map behavior
+except ImportError:
+    pass
+
+# Some magic here.  If _use_slots is True, the classes will derive from
+# object and will define a __slots__ class variable.  If _use_slots is
+# False, classes will be old-style and will not define __slots__.
+#
+# _use_slots = True:   Memory efficient, probably faster in future versions
+#                      of Python, "better".
+# _use_slots = False:  Ordinary classes, much faster than slots in current
+#                      versions of Python (2.4 and 2.5).
+_use_slots = True
+
+# If True, allows components of Vector2 and Vector3 to be set via swizzling;
+# e.g.  v.xyz = (1, 2, 3).  This is much, much slower than the more verbose
+# v.x = 1; v.y = 2; v.z = 3,  and slows down ordinary element setting as
+# well.  Recommended setting is False.
+_enable_swizzle_set = False
+
+# Requires class to derive from object.
+if _enable_swizzle_set:
+    _use_slots = True
+
+
+# Implement _use_slots magic.
+class _EuclidMetaclass(type):
+    def __new__(cls, name, bases, dct):
+        if '__slots__' in dct:
+            dct['__getstate__'] = cls._create_getstate(dct['__slots__'])
+            dct['__setstate__'] = cls._create_setstate(dct['__slots__'])
+        if _use_slots:
+            return type.__new__(cls, name, bases + (object,), dct)
+        else:
+            if '__slots__' in dct:
+                del dct['__slots__']
+            return types.ClassType.__new__(type, name, bases, dct)
+
+    @classmethod
+    def _create_getstate(cls, slots):
+        def __getstate__(self):
+            d = {}
+            for slot in slots:
+                d[slot] = getattr(self, slot)
+            return d
+
+        return __getstate__
+
+    @classmethod
+    def _create_setstate(cls, slots):
+        def __setstate__(self, state):
+            for name, value in list(state.items()):
+                setattr(self, name, value)
+
+        return __setstate__
+
+
+__metaclass__ = _EuclidMetaclass
+
+
+class Vector2:
+    __slots__ = ['x', 'y']
+
+    def __init__(self, x, y):
+        self.x = x
+        self.y = y
+
+    def __copy__(self):
+        return self.__class__(self.x, self.y)
+
+    copy = __copy__
+
+    def __repr__(self):
+        return 'Vector2(%.2f, %.2f)' % (self.x, self.y)
+
+    def __eq__(self, other):
+        if isinstance(other, Vector2):
+            return self.x == other.x and \
+                   self.y == other.y
+        else:
+            assert hasattr(other, '__len__') and len(other) == 2
+            return self.x == other[0] and \
+                   self.y == other[1]
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __bool__(self):
+        return self.x != 0 or self.y != 0
+
+    def __len__(self):
+        return 2
+
+    def __getitem__(self, key):
+        return (self.x, self.y)[key]
+
+    def __setitem__(self, key, value):
+        l = [self.x, self.y]
+        l[key] = value
+        self.x, self.y = l
+
+    def __iter__(self):
+        return iter((self.x, self.y))
+
+    def __getattr__(self, name):
+        try:
+            return tuple([(self.x, self.y)['xy'.index(c)] \
+                          for c in name])
+        except ValueError:
+            raise AttributeError(name)
+
+    if _enable_swizzle_set:
+        # This has detrimental performance on ordinary setattr as well
+        # if enabled
+        def __setattr__(self, name, value):
+            if len(name) == 1:
+                object.__setattr__(self, name, value)
+            else:
+                try:
+                    l = [self.x, self.y]
+                    for c, v in map(None, name, value):
+                        l['xy'.index(c)] = v
+                    self.x, self.y = l
+                except ValueError:
+                    raise AttributeError(name)
+
+    def __add__(self, other):
+        if isinstance(other, Vector2):
+            return Vector2(self.x + other.x,
+                           self.y + other.y)
+        else:
+            assert hasattr(other, '__len__') and len(other) == 2
+            return Vector2(self.x + other[0],
+                           self.y + other[1])
+
+    __radd__ = __add__
+
+    def __iadd__(self, other):
+        if isinstance(other, Vector2):
+            self.x += other.x
+            self.y += other.y
+        else:
+            self.x += other[0]
+            self.y += other[1]
+        return self
+
+    def __sub__(self, other):
+        if isinstance(other, Vector2):
+            return Vector2(self.x - other.x,
+                           self.y - other.y)
+        else:
+            assert hasattr(other, '__len__') and len(other) == 2
+            return Vector2(self.x - other[0],
+                           self.y - other[1])
+
+    def __rsub__(self, other):
+        if isinstance(other, Vector2):
+            return Vector2(other.x - self.x,
+                           other.y - self.y)
+        else:
+            assert hasattr(other, '__len__') and len(other) == 2
+            return Vector2(other.x - self[0],
+                           other.y - self[1])
+
+    def __mul__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector2(self.x * other,
+                       self.y * other)
+
+    __rmul__ = __mul__
+
+    def __imul__(self, other):
+        assert type(other) in (int, int, float)
+        self.x *= other
+        self.y *= other
+        return self
+
+    def __div__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector2(operator.div(self.x, other),
+                       operator.div(self.y, other))
+
+    def __rdiv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector2(operator.div(other, self.x),
+                       operator.div(other, self.y))
+
+    def __floordiv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector2(operator.floordiv(self.x, other),
+                       operator.floordiv(self.y, other))
+
+    def __rfloordiv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector2(operator.floordiv(other, self.x),
+                       operator.floordiv(other, self.y))
+
+    def __truediv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector2(operator.truediv(self.x, other),
+                       operator.truediv(self.y, other))
+
+    def __rtruediv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector2(operator.truediv(other, self.x),
+                       operator.truediv(other, self.y))
+
+    def __neg__(self):
+        return Vector2(-self.x,
+                       -self.y)
+
+    __pos__ = __copy__
+
+    def __abs__(self):
+        return math.sqrt(self.x ** 2 + \
+                         self.y ** 2)
+
+    magnitude = __abs__
+
+    def magnitude_squared(self):
+        return self.x ** 2 + \
+               self.y ** 2
+
+    def normalize(self):
+        d = self.magnitude()
+        if d:
+            self.x /= d
+            self.y /= d
+        return self
+
+    def normalized(self):
+        d = self.magnitude()
+        if d:
+            return Vector2(self.x / d,
+                           self.y / d)
+        return self.copy()
+
+    def dot(self, other):
+        assert isinstance(other, Vector2)
+        return self.x * other.x + \
+               self.y * other.y
+
+    def cross(self):
+        return Vector2(self.y, -self.x)
+
+    def reflect(self, normal):
+        # assume normal is normalized
+        assert isinstance(normal, Vector2)
+        d = 2 * (self.x * normal.x + self.y * normal.y)
+        return Vector2(self.x - d * normal.x,
+                       self.y - d * normal.y)
+
+
+class Vector3:
+    __slots__ = ['x', 'y', 'z']
+
+    def __init__(self, x, y, z):
+        self.x = x
+        self.y = y
+        self.z = z
+
+    def __copy__(self):
+        return self.__class__(self.x, self.y, self.z)
+
+    copy = __copy__
+
+    def __repr__(self):
+        return 'Vector3(%.2f, %.2f, %.2f)' % (self.x,
+                                              self.y,
+                                              self.z)
+
+    def __eq__(self, other):
+        if isinstance(other, Vector3):
+            return self.x == other.x and \
+                   self.y == other.y and \
+                   self.z == other.z
+        else:
+            assert hasattr(other, '__len__') and len(other) == 3
+            return self.x == other[0] and \
+                   self.y == other[1] and \
+                   self.z == other[2]
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __bool__(self):
+        return self.x != 0 or self.y != 0 or self.z != 0
+
+    def __len__(self):
+        return 3
+
+    def __getitem__(self, key):
+        return (self.x, self.y, self.z)[key]
+
+    def __setitem__(self, key, value):
+        l = [self.x, self.y, self.z]
+        l[key] = value
+        self.x, self.y, self.z = l
+
+    def __iter__(self):
+        return iter((self.x, self.y, self.z))
+
+    def __getattr__(self, name):
+        try:
+            return tuple([(self.x, self.y, self.z)['xyz'.index(c)] \
+                          for c in name])
+        except ValueError:
+            raise AttributeError(name)
+
+    if _enable_swizzle_set:
+        # This has detrimental performance on ordinary setattr as well
+        # if enabled
+        def __setattr__(self, name, value):
+            if len(name) == 1:
+                object.__setattr__(self, name, value)
+            else:
+                try:
+                    l = [self.x, self.y, self.z]
+                    for c, v in map(None, name, value):
+                        l['xyz'.index(c)] = v
+                    self.x, self.y, self.z = l
+                except ValueError:
+                    raise AttributeError(name)
+
+    def __add__(self, other):
+        if isinstance(other, Vector3):
+            # Vector + Vector -> Vector
+            # Vector + Point -> Point
+            # Point + Point -> Vector
+            if self.__class__ is other.__class__:
+                _class = Vector3
+            else:
+                _class = Point3
+            return _class(self.x + other.x,
+                          self.y + other.y,
+                          self.z + other.z)
+        else:
+            assert hasattr(other, '__len__') and len(other) == 3
+            return Vector3(self.x + other[0],
+                           self.y + other[1],
+                           self.z + other[2])
+
+    __radd__ = __add__
+
+    def __iadd__(self, other):
+        if isinstance(other, Vector3):
+            self.x += other.x
+            self.y += other.y
+            self.z += other.z
+        else:
+            self.x += other[0]
+            self.y += other[1]
+            self.z += other[2]
+        return self
+
+    def __sub__(self, other):
+        if isinstance(other, Vector3):
+            # Vector - Vector -> Vector
+            # Vector - Point -> Point
+            # Point - Point -> Vector
+            if self.__class__ is other.__class__:
+                _class = Vector3
+            else:
+                _class = Point3
+            return Vector3(self.x - other.x,
+                           self.y - other.y,
+                           self.z - other.z)
+        else:
+            assert hasattr(other, '__len__') and len(other) == 3
+            return Vector3(self.x - other[0],
+                           self.y - other[1],
+                           self.z - other[2])
+
+    def __rsub__(self, other):
+        if isinstance(other, Vector3):
+            return Vector3(other.x - self.x,
+                           other.y - self.y,
+                           other.z - self.z)
+        else:
+            assert hasattr(other, '__len__') and len(other) == 3
+            return Vector3(other.x - self[0],
+                           other.y - self[1],
+                           other.z - self[2])
+
+    def __mul__(self, other):
+        if isinstance(other, Vector3):
+            # TODO component-wise mul/div in-place and on Vector2; docs.
+            if self.__class__ is Point3 or other.__class__ is Point3:
+                _class = Point3
+            else:
+                _class = Vector3
+            return _class(self.x * other.x,
+                          self.y * other.y,
+                          self.z * other.z)
+        else:
+            assert type(other) in (int, int, float)
+            return Vector3(self.x * other,
+                           self.y * other,
+                           self.z * other)
+
+    __rmul__ = __mul__
+
+    def __imul__(self, other):
+        assert type(other) in (int, int, float)
+        self.x *= other
+        self.y *= other
+        self.z *= other
+        return self
+
+    def __div__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector3(operator.div(self.x, other),
+                       operator.div(self.y, other),
+                       operator.div(self.z, other))
+
+    def __rdiv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector3(operator.div(other, self.x),
+                       operator.div(other, self.y),
+                       operator.div(other, self.z))
+
+    def __floordiv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector3(operator.floordiv(self.x, other),
+                       operator.floordiv(self.y, other),
+                       operator.floordiv(self.z, other))
+
+    def __rfloordiv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector3(operator.floordiv(other, self.x),
+                       operator.floordiv(other, self.y),
+                       operator.floordiv(other, self.z))
+
+    def __truediv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector3(operator.truediv(self.x, other),
+                       operator.truediv(self.y, other),
+                       operator.truediv(self.z, other))
+
+    def __rtruediv__(self, other):
+        assert type(other) in (int, int, float)
+        return Vector3(operator.truediv(other, self.x),
+                       operator.truediv(other, self.y),
+                       operator.truediv(other, self.z))
+
+    def __neg__(self):
+        return Vector3(-self.x,
+                       -self.y,
+                       -self.z)
+
+    __pos__ = __copy__
+
+    def __abs__(self):
+        return math.sqrt(self.x ** 2 + \
+                         self.y ** 2 + \
+                         self.z ** 2)
+
+    magnitude = __abs__
+
+    def magnitude_squared(self):
+        return self.x ** 2 + \
+               self.y ** 2 + \
+               self.z ** 2
+
+    def normalize(self):
+        d = self.magnitude()
+        if d:
+            self.x /= d
+            self.y /= d
+            self.z /= d
+        return self
+
+    def normalized(self):
+        d = self.magnitude()
+        if d:
+            return Vector3(self.x / d,
+                           self.y / d,
+                           self.z / d)
+        return self.copy()
+
+    def dot(self, other):
+        assert isinstance(other, Vector3)
+        return self.x * other.x + \
+               self.y * other.y + \
+               self.z * other.z
+
+    def cross(self, other):
+        assert isinstance(other, Vector3)
+        return Vector3(self.y * other.z - self.z * other.y,
+                       -self.x * other.z + self.z * other.x,
+                       self.x * other.y - self.y * other.x)
+
+    def reflect(self, normal):
+        # assume normal is normalized
+        assert isinstance(normal, Vector3)
+        d = 2 * (self.x * normal.x + self.y * normal.y + self.z * normal.z)
+        return Vector3(self.x - d * normal.x,
+                       self.y - d * normal.y,
+                       self.z - d * normal.z)
+
+
+class AffineVector3(Vector3):
+    w = 1
+
+    def __repr__(self):
+        return 'Vector3(%.2f, %.2f, %.2f, 1.00)' % (self.x,
+                                                    self.y,
+                                                    self.z)
+
+    def __len__(self):
+        return 4
+
+    def __getitem__(self, key):
+        return (self.x, self.y, self.z, 1)[key]
+
+    def __iter__(self):
+        return iter((self.x, self.y, self.z, 1))
+
+
+# a b c
+# e f g
+# i j k
+
+class Matrix3:
+    __slots__ = list('abcefgijk')
+
+    def __init__(self):
+        self.identity()
+
+    def __copy__(self):
+        M = Matrix3()
+        M.a = self.a
+        M.b = self.b
+        M.c = self.c
+        M.e = self.e
+        M.f = self.f
+        M.g = self.g
+        M.i = self.i
+        M.j = self.j
+        M.k = self.k
+        return M
+
+    copy = __copy__
+
+    def __repr__(self):
+        return ('Matrix3([% 8.2f % 8.2f % 8.2f\n' \
+                '         % 8.2f % 8.2f % 8.2f\n' \
+                '         % 8.2f % 8.2f % 8.2f])') \
+               % (self.a, self.b, self.c,
+                  self.e, self.f, self.g,
+                  self.i, self.j, self.k)
+
+    def __getitem__(self, key):
+        return [self.a, self.e, self.i,
+                self.b, self.f, self.j,
+                self.c, self.g, self.k][key]
+
+    def __setitem__(self, key, value):
+        L = self[:]
+        L[key] = value
+        (self.a, self.e, self.i,
+         self.b, self.f, self.j,
+         self.c, self.g, self.k) = L
+
+    def __mul__(self, other):
+        if isinstance(other, Matrix3):
+            # Caching repeatedly accessed attributes in local variables
+            # apparently increases performance by 20%.  Attrib: Will McGugan.
+            Aa = self.a
+            Ab = self.b
+            Ac = self.c
+            Ae = self.e
+            Af = self.f
+            Ag = self.g
+            Ai = self.i
+            Aj = self.j
+            Ak = self.k
+            Ba = other.a
+            Bb = other.b
+            Bc = other.c
+            Be = other.e
+            Bf = other.f
+            Bg = other.g
+            Bi = other.i
+            Bj = other.j
+            Bk = other.k
+            C = Matrix3()
+            C.a = Aa * Ba + Ab * Be + Ac * Bi
+            C.b = Aa * Bb + Ab * Bf + Ac * Bj
+            C.c = Aa * Bc + Ab * Bg + Ac * Bk
+            C.e = Ae * Ba + Af * Be + Ag * Bi
+            C.f = Ae * Bb + Af * Bf + Ag * Bj
+            C.g = Ae * Bc + Af * Bg + Ag * Bk
+            C.i = Ai * Ba + Aj * Be + Ak * Bi
+            C.j = Ai * Bb + Aj * Bf + Ak * Bj
+            C.k = Ai * Bc + Aj * Bg + Ak * Bk
+            return C
+        elif isinstance(other, Point2):
+            A = self
+            B = other
+            P = Point2(0, 0)
+            P.x = A.a * B.x + A.b * B.y + A.c
+            P.y = A.e * B.x + A.f * B.y + A.g
+            return P
+        elif isinstance(other, Vector2):
+            A = self
+            B = other
+            V = Vector2(0, 0)
+            V.x = A.a * B.x + A.b * B.y
+            V.y = A.e * B.x + A.f * B.y
+            return V
+        else:
+            other = other.copy()
+            other._apply_transform(self)
+            return other
+
+    def __imul__(self, other):
+        assert isinstance(other, Matrix3)
+        # Cache attributes in local vars (see Matrix3.__mul__).
+        Aa = self.a
+        Ab = self.b
+        Ac = self.c
+        Ae = self.e
+        Af = self.f
+        Ag = self.g
+        Ai = self.i
+        Aj = self.j
+        Ak = self.k
+        Ba = other.a
+        Bb = other.b
+        Bc = other.c
+        Be = other.e
+        Bf = other.f
+        Bg = other.g
+        Bi = other.i
+        Bj = other.j
+        Bk = other.k
+        self.a = Aa * Ba + Ab * Be + Ac * Bi
+        self.b = Aa * Bb + Ab * Bf + Ac * Bj
+        self.c = Aa * Bc + Ab * Bg + Ac * Bk
+        self.e = Ae * Ba + Af * Be + Ag * Bi
+        self.f = Ae * Bb + Af * Bf + Ag * Bj
+        self.g = Ae * Bc + Af * Bg + Ag * Bk
+        self.i = Ai * Ba + Aj * Be + Ak * Bi
+        self.j = Ai * Bb + Aj * Bf + Ak * Bj
+        self.k = Ai * Bc + Aj * Bg + Ak * Bk
+        return self
+
+    def identity(self):
+        self.a = self.f = self.k = 1.
+        self.b = self.c = self.e = self.g = self.i = self.j = 0
+        return self
+
+    def scale(self, x, y):
+        self *= Matrix3.new_scale(x, y)
+        return self
+
+    def translate(self, x, y):
+        self *= Matrix3.new_translate(x, y)
+        return self
+
+    def rotate(self, angle):
+        self *= Matrix3.new_rotate(angle)
+        return self
+
+    # Static constructors
+    def new_identity(cls):
+        self = cls()
+        return self
+
+    new_identity = classmethod(new_identity)
+
+    def new_scale(cls, x, y):
+        self = cls()
+        self.a = x
+        self.f = y
+        return self
+
+    new_scale = classmethod(new_scale)
+
+    def new_translate(cls, x, y):
+        self = cls()
+        self.c = x
+        self.g = y
+        return self
+
+    new_translate = classmethod(new_translate)
+
+    def new_rotate(cls, angle):
+        self = cls()
+        s = math.sin(angle)
+        c = math.cos(angle)
+        self.a = self.f = c
+        self.b = -s
+        self.e = s
+        return self
+
+    new_rotate = classmethod(new_rotate)
+
+
+# a b c d
+# e f g h
+# i j k l
+# m n o p
+
+class Matrix4:
+    __slots__ = list('abcdefghijklmnop')
+
+    def __init__(self):
+        self.identity()
+
+    def __copy__(self):
+        M = Matrix4()
+        M.a = self.a
+        M.b = self.b
+        M.c = self.c
+        M.d = self.d
+        M.e = self.e
+        M.f = self.f
+        M.g = self.g
+        M.h = self.h
+        M.i = self.i
+        M.j = self.j
+        M.k = self.k
+        M.l = self.l
+        M.m = self.m
+        M.n = self.n
+        M.o = self.o
+        M.p = self.p
+        return M
+
+    copy = __copy__
+
+    def __repr__(self):
+        return ('Matrix4([% 8.2f % 8.2f % 8.2f % 8.2f\n' \
+                '         % 8.2f % 8.2f % 8.2f % 8.2f\n' \
+                '         % 8.2f % 8.2f % 8.2f % 8.2f\n' \
+                '         % 8.2f % 8.2f % 8.2f % 8.2f])') \
+               % (self.a, self.b, self.c, self.d,
+                  self.e, self.f, self.g, self.h,
+                  self.i, self.j, self.k, self.l,
+                  self.m, self.n, self.o, self.p)
+
+    def __getitem__(self, key):
+        return [self.a, self.e, self.i, self.m,
+                self.b, self.f, self.j, self.n,
+                self.c, self.g, self.k, self.o,
+                self.d, self.h, self.l, self.p][key]
+
+    def __setitem__(self, key, value):
+        assert not isinstance(key, slice) or \
+               key.stop - key.start == len(value), 'key length != value length'
+        L = self[:]
+        L[key] = value
+        (self.a, self.e, self.i, self.m,
+         self.b, self.f, self.j, self.n,
+         self.c, self.g, self.k, self.o,
+         self.d, self.h, self.l, self.p) = L
+
+    def __mul__(self, other):
+        if isinstance(other, Matrix4):
+            # Cache attributes in local vars (see Matrix3.__mul__).
+            Aa = self.a
+            Ab = self.b
+            Ac = self.c
+            Ad = self.d
+            Ae = self.e
+            Af = self.f
+            Ag = self.g
+            Ah = self.h
+            Ai = self.i
+            Aj = self.j
+            Ak = self.k
+            Al = self.l
+            Am = self.m
+            An = self.n
+            Ao = self.o
+            Ap = self.p
+            Ba = other.a
+            Bb = other.b
+            Bc = other.c
+            Bd = other.d
+            Be = other.e
+            Bf = other.f
+            Bg = other.g
+            Bh = other.h
+            Bi = other.i
+            Bj = other.j
+            Bk = other.k
+            Bl = other.l
+            Bm = other.m
+            Bn = other.n
+            Bo = other.o
+            Bp = other.p
+            C = Matrix4()
+            C.a = Aa * Ba + Ab * Be + Ac * Bi + Ad * Bm
+            C.b = Aa * Bb + Ab * Bf + Ac * Bj + Ad * Bn
+            C.c = Aa * Bc + Ab * Bg + Ac * Bk + Ad * Bo
+            C.d = Aa * Bd + Ab * Bh + Ac * Bl + Ad * Bp
+            C.e = Ae * Ba + Af * Be + Ag * Bi + Ah * Bm
+            C.f = Ae * Bb + Af * Bf + Ag * Bj + Ah * Bn
+            C.g = Ae * Bc + Af * Bg + Ag * Bk + Ah * Bo
+            C.h = Ae * Bd + Af * Bh + Ag * Bl + Ah * Bp
+            C.i = Ai * Ba + Aj * Be + Ak * Bi + Al * Bm
+            C.j = Ai * Bb + Aj * Bf + Ak * Bj + Al * Bn
+            C.k = Ai * Bc + Aj * Bg + Ak * Bk + Al * Bo
+            C.l = Ai * Bd + Aj * Bh + Ak * Bl + Al * Bp
+            C.m = Am * Ba + An * Be + Ao * Bi + Ap * Bm
+            C.n = Am * Bb + An * Bf + Ao * Bj + Ap * Bn
+            C.o = Am * Bc + An * Bg + Ao * Bk + Ap * Bo
+            C.p = Am * Bd + An * Bh + Ao * Bl + Ap * Bp
+            return C
+        elif isinstance(other, Point3):
+            A = self
+            B = other
+            P = Point3(0, 0, 0)
+            P.x = A.a * B.x + A.b * B.y + A.c * B.z + A.d
+            P.y = A.e * B.x + A.f * B.y + A.g * B.z + A.h
+            P.z = A.i * B.x + A.j * B.y + A.k * B.z + A.l
+            return P
+        elif isinstance(other, AffineVector3):
+            A = self
+            B = other
+            V = AffineVector3(0, 0, 0)
+            V.x = A.a * B.x + A.b * B.y + A.c * B.z + A.d * B.w
+            V.y = A.e * B.x + A.f * B.y + A.g * B.z + A.h * B.w
+            V.z = A.i * B.x + A.j * B.y + A.k * B.z + A.l * B.w
+            return V
+        elif isinstance(other, Vector3):
+            A = self
+            B = other
+            V = Vector3(0, 0, 0)
+            V.x = A.a * B.x + A.b * B.y + A.c * B.z
+            V.y = A.e * B.x + A.f * B.y + A.g * B.z
+            V.z = A.i * B.x + A.j * B.y + A.k * B.z
+            return V
+        else:
+            other = other.copy()
+            other._apply_transform(self)
+            return other
+
+    def __imul__(self, other):
+        assert isinstance(other, Matrix4)
+        # Cache attributes in local vars (see Matrix3.__mul__).
+        Aa = self.a
+        Ab = self.b
+        Ac = self.c
+        Ad = self.d
+        Ae = self.e
+        Af = self.f
+        Ag = self.g
+        Ah = self.h
+        Ai = self.i
+        Aj = self.j
+        Ak = self.k
+        Al = self.l
+        Am = self.m
+        An = self.n
+        Ao = self.o
+        Ap = self.p
+        Ba = other.a
+        Bb = other.b
+        Bc = other.c
+        Bd = other.d
+        Be = other.e
+        Bf = other.f
+        Bg = other.g
+        Bh = other.h
+        Bi = other.i
+        Bj = other.j
+        Bk = other.k
+        Bl = other.l
+        Bm = other.m
+        Bn = other.n
+        Bo = other.o
+        Bp = other.p
+        self.a = Aa * Ba + Ab * Be + Ac * Bi + Ad * Bm
+        self.b = Aa * Bb + Ab * Bf + Ac * Bj + Ad * Bn
+        self.c = Aa * Bc + Ab * Bg + Ac * Bk + Ad * Bo
+        self.d = Aa * Bd + Ab * Bh + Ac * Bl + Ad * Bp
+        self.e = Ae * Ba + Af * Be + Ag * Bi + Ah * Bm
+        self.f = Ae * Bb + Af * Bf + Ag * Bj + Ah * Bn
+        self.g = Ae * Bc + Af * Bg + Ag * Bk + Ah * Bo
+        self.h = Ae * Bd + Af * Bh + Ag * Bl + Ah * Bp
+        self.i = Ai * Ba + Aj * Be + Ak * Bi + Al * Bm
+        self.j = Ai * Bb + Aj * Bf + Ak * Bj + Al * Bn
+        self.k = Ai * Bc + Aj * Bg + Ak * Bk + Al * Bo
+        self.l = Ai * Bd + Aj * Bh + Ak * Bl + Al * Bp
+        self.m = Am * Ba + An * Be + Ao * Bi + Ap * Bm
+        self.n = Am * Bb + An * Bf + Ao * Bj + Ap * Bn
+        self.o = Am * Bc + An * Bg + Ao * Bk + Ap * Bo
+        self.p = Am * Bd + An * Bh + Ao * Bl + Ap * Bp
+        return self
+
+    def identity(self):
+        self.a = self.f = self.k = self.p = 1.
+        self.b = self.c = self.d = self.e = self.g = self.h = \
+            self.i = self.j = self.l = self.m = self.n = self.o = 0
+        return self
+
+    def scale(self, x, y, z):
+        self *= Matrix4.new_scale(x, y, z)
+        return self
+
+    def translate(self, x, y, z):
+        self *= Matrix4.new_translate(x, y, z)
+        return self
+
+    def rotatex(self, angle):
+        self *= Matrix4.new_rotatex(angle)
+        return self
+
+    def rotatey(self, angle):
+        self *= Matrix4.new_rotatey(angle)
+        return self
+
+    def rotatez(self, angle):
+        self *= Matrix4.new_rotatez(angle)
+        return self
+
+    def rotate_axis(self, angle, axis):
+        self *= Matrix4.new_rotate_axis(angle, axis)
+        return self
+
+    def rotate_euler(self, heading, attitude, bank):
+        self *= Matrix4.new_rotate_euler(heading, attitude, bank)
+        return self
+
+    # Static constructors
+    def new_identity(cls):
+        self = cls()
+        return self
+
+    new_identity = classmethod(new_identity)
+
+    def new_scale(cls, x, y, z):
+        self = cls()
+        self.a = x
+        self.f = y
+        self.k = z
+        return self
+
+    new_scale = classmethod(new_scale)
+
+    def new_translate(cls, x, y, z):
+        self = cls()
+        self.d = x
+        self.h = y
+        self.l = z
+        return self
+
+    new_translate = classmethod(new_translate)
+
+    def new_rotatex(cls, angle):
+        self = cls()
+        s = math.sin(angle)
+        c = math.cos(angle)
+        self.f = self.k = c
+        self.g = -s
+        self.j = s
+        return self
+
+    new_rotatex = classmethod(new_rotatex)
+
+    def new_rotatey(cls, angle):
+        self = cls()
+        s = math.sin(angle)
+        c = math.cos(angle)
+        self.a = self.k = c
+        self.c = s
+        self.i = -s
+        return self
+
+    new_rotatey = classmethod(new_rotatey)
+
+    def new_rotatez(cls, angle):
+        self = cls()
+        s = math.sin(angle)
+        c = math.cos(angle)
+        self.a = self.f = c
+        self.b = -s
+        self.e = s
+        return self
+
+    new_rotatez = classmethod(new_rotatez)
+
+    def new_rotate_axis(cls, angle, axis):
+        assert (isinstance(axis, Vector3))
+        vector = axis.normalized()
+        x = vector.x
+        y = vector.y
+        z = vector.z
+
+        self = cls()
+        s = math.sin(angle)
+        c = math.cos(angle)
+        c1 = 1. - c
+
+        # from the glRotate man page
+        self.a = x * x * c1 + c
+        self.b = x * y * c1 - z * s
+        self.c = x * z * c1 + y * s
+        self.e = y * x * c1 + z * s
+        self.f = y * y * c1 + c
+        self.g = y * z * c1 - x * s
+        self.i = x * z * c1 - y * s
+        self.j = y * z * c1 + x * s
+        self.k = z * z * c1 + c
+        return self
+
+    new_rotate_axis = classmethod(new_rotate_axis)
+
+    def new_rotate_euler(cls, heading, attitude, bank):
+        # from http://www.euclideanspace.com/
+        ch = math.cos(heading)
+        sh = math.sin(heading)
+        ca = math.cos(attitude)
+        sa = math.sin(attitude)
+        cb = math.cos(bank)
+        sb = math.sin(bank)
+
+        self = cls()
+        self.a = ch * ca
+        self.b = sh * sb - ch * sa * cb
+        self.c = ch * sa * sb + sh * cb
+        self.e = sa
+        self.f = ca * cb
+        self.g = -ca * sb
+        self.i = -sh * ca
+        self.j = sh * sa * cb + ch * sb
+        self.k = -sh * sa * sb + ch * cb
+        return self
+
+    new_rotate_euler = classmethod(new_rotate_euler)
+
+    def new_perspective(cls, fov_y, aspect, near, far):
+        # from the gluPerspective man page
+        f = 1 / math.tan(fov_y / 2)
+        self = cls()
+        assert near != 0.0 and near != far
+        self.a = f / aspect
+        self.f = f
+        self.k = (far + near) / (near - far)
+        self.l = 2 * far * near / (near - far)
+        self.o = -1
+        self.p = 0
+        return self
+
+    new_perspective = classmethod(new_perspective)
+
+
+class Quaternion:
+    # All methods and naming conventions based off
+    # http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions
+
+    # w is the real part, (x, y, z) are the imaginary parts
+    __slots__ = ['w', 'x', 'y', 'z']
+
+    def __init__(self):
+        self.identity()
+
+    def __copy__(self):
+        Q = Quaternion()
+        Q.w = self.w
+        Q.x = self.x
+        Q.y = self.y
+        Q.z = self.z
+
+    copy = __copy__
+
+    def __repr__(self):
+        return 'Quaternion(real=%.2f, imag=<%.2f, %.2f, %.2f>)' % \
+               (self.w, self.x, self.y, self.z)
+
+    def __mul__(self, other):
+        if isinstance(other, Quaternion):
+            Ax = self.x
+            Ay = self.y
+            Az = self.z
+            Aw = self.w
+            Bx = other.x
+            By = other.y
+            Bz = other.z
+            Bw = other.w
+            Q = Quaternion()
+            Q.x = Ax * Bw + Ay * Bz - Az * By + Aw * Bx
+            Q.y = -Ax * Bz + Ay * Bw + Az * Bx + Aw * By
+            Q.z = Ax * By - Ay * Bx + Az * Bw + Aw * Bz
+            Q.w = -Ax * Bx - Ay * By - Az * Bz + Aw * Bw
+            return Q
+        elif isinstance(other, Vector3):
+            w = self.w
+            x = self.x
+            y = self.y
+            z = self.z
+            Vx = other.x
+            Vy = other.y
+            Vz = other.z
+            return other.__class__( \
+                w * w * Vx + 2 * y * w * Vz - 2 * z * w * Vy + \
+                x * x * Vx + 2 * y * x * Vy + 2 * z * x * Vz - \
+                z * z * Vx - y * y * Vx,
+                2 * x * y * Vx + y * y * Vy + 2 * z * y * Vz + \
+                2 * w * z * Vx - z * z * Vy + w * w * Vy - \
+                2 * x * w * Vz - x * x * Vy,
+                2 * x * z * Vx + 2 * y * z * Vy + \
+                z * z * Vz - 2 * w * y * Vx - y * y * Vz + \
+                2 * w * x * Vy - x * x * Vz + w * w * Vz)
+        else:
+            other = other.copy()
+            other._apply_transform(self)
+            return other
+
+    def __imul__(self, other):
+        assert isinstance(other, Quaternion)
+        Ax = self.x
+        Ay = self.y
+        Az = self.z
+        Aw = self.w
+        Bx = other.x
+        By = other.y
+        Bz = other.z
+        Bw = other.w
+        self.x = Ax * Bw + Ay * Bz - Az * By + Aw * Bx
+        self.y = -Ax * Bz + Ay * Bw + Az * Bx + Aw * By
+        self.z = Ax * By - Ay * Bx + Az * Bw + Aw * Bz
+        self.w = -Ax * Bx - Ay * By - Az * Bz + Aw * Bw
+        return self
+
+    def __abs__(self):
+        return math.sqrt(self.w ** 2 + \
+                         self.x ** 2 + \
+                         self.y ** 2 + \
+                         self.z ** 2)
+
+    magnitude = __abs__
+
+    def magnitude_squared(self):
+        return self.w ** 2 + \
+               self.x ** 2 + \
+               self.y ** 2 + \
+               self.z ** 2
+
+    def identity(self):
+        self.w = 1
+        self.x = 0
+        self.y = 0
+        self.z = 0
+        return self
+
+    def rotate_axis(self, angle, axis):
+        self *= Quaternion.new_rotate_axis(angle, axis)
+        return self
+
+    def rotate_euler(self, heading, attitude, bank):
+        self *= Quaternion.new_rotate_euler(heading, attitude, bank)
+        return self
+
+    def conjugated(self):
+        Q = Quaternion()
+        Q.w = self.w
+        Q.x = -self.x
+        Q.y = -self.y
+        Q.z = -self.z
+        return Q
+
+    def normalize(self):
+        d = self.magnitude()
+        if d != 0:
+            self.w /= d
+            self.x /= d
+            self.y /= d
+            self.z /= d
+        return self
+
+    def normalized(self):
+        d = self.magnitude()
+        if d != 0:
+            Q = Quaternion()
+            Q.w /= d
+            Q.x /= d
+            Q.y /= d
+            Q.z /= d
+            return Q
+        else:
+            return self.copy()
+
+    def get_angle_axis(self):
+        if self.w > 1:
+            self = self.normalized()
+        angle = 2 * math.acos(self.w)
+        s = math.sqrt(1 - self.w ** 2)
+        if s < 0.001:
+            return angle, Vector3(1, 0, 0)
+        else:
+            return angle, Vector3(self.x / s, self.y / s, self.z / s)
+
+    def get_euler(self):
+        t = self.x * self.y + self.z * self.w
+        if t > 0.4999:
+            heading = 2 * math.atan2(self.x, self.w)
+            attitude = math.pi / 2
+            bank = 0
+        elif t < -0.4999:
+            heading = -2 * math.atan2(self.x, self.w)
+            attitude = -math.pi / 2
+            bank = 0
+        else:
+            sqx = self.x ** 2
+            sqy = self.y ** 2
+            sqz = self.z ** 2
+            heading = math.atan2(2 * self.y * self.w - 2 * self.x * self.z,
+                                 1 - 2 * sqy - 2 * sqz)
+            attitude = math.asin(2 * t)
+            bank = math.atan2(2 * self.x * self.w - 2 * self.y * self.z,
+                              1 - 2 * sqx - 2 * sqz)
+        return heading, attitude, bank
+
+    def get_matrix(self):
+        xx = self.x ** 2
+        xy = self.x * self.y
+        xz = self.x * self.z
+        xw = self.x * self.w
+        yy = self.y ** 2
+        yz = self.y * self.z
+        yw = self.y * self.w
+        zz = self.z ** 2
+        zw = self.z * self.w
+        M = Matrix4()
+        M.a = 1 - 2 * (yy + zz)
+        M.b = 2 * (xy - zw)
+        M.c = 2 * (xz + yw)
+        M.e = 2 * (xy + zw)
+        M.f = 1 - 2 * (xx + zz)
+        M.g = 2 * (yz - xw)
+        M.i = 2 * (xz - yw)
+        M.j = 2 * (yz + xw)
+        M.k = 1 - 2 * (xx + yy)
+        return M
+
+    # Static constructors
+    def new_identity(cls):
+        return cls()
+
+    new_identity = classmethod(new_identity)
+
+    def new_rotate_axis(cls, angle, axis):
+        assert (isinstance(axis, Vector3))
+        axis = axis.normalized()
+        s = math.sin(angle / 2)
+        Q = cls()
+        Q.w = math.cos(angle / 2)
+        Q.x = axis.x * s
+        Q.y = axis.y * s
+        Q.z = axis.z * s
+        return Q
+
+    new_rotate_axis = classmethod(new_rotate_axis)
+
+    def new_rotate_euler(cls, heading, attitude, bank):
+        Q = cls()
+        c1 = math.cos(heading / 2)
+        s1 = math.sin(heading / 2)
+        c2 = math.cos(attitude / 2)
+        s2 = math.sin(attitude / 2)
+        c3 = math.cos(bank / 2)
+        s3 = math.sin(bank / 2)
+
+        Q.w = c1 * c2 * c3 - s1 * s2 * s3
+        Q.x = s1 * s2 * c3 + c1 * c2 * s3
+        Q.y = s1 * c2 * c3 + c1 * s2 * s3
+        Q.z = c1 * s2 * c3 - s1 * c2 * s3
+        return Q
+
+    new_rotate_euler = classmethod(new_rotate_euler)
+
+    def new_interpolate(cls, q1, q2, t):
+        assert isinstance(q1, Quaternion) and isinstance(q2, Quaternion)
+        Q = cls()
+
+        costheta = q1.w * q2.w + q1.x * q2.x + q1.y * q2.y + q1.z * q2.z
+        theta = math.acos(costheta)
+        if abs(theta) < 0.01:
+            Q.w = q2.w
+            Q.x = q2.x
+            Q.y = q2.y
+            Q.z = q2.z
+            return Q
+
+        sintheta = math.sqrt(1.0 - costheta * costheta)
+        if abs(sintheta) < 0.01:
+            Q.w = (q1.w + q2.w) * 0.5
+            Q.x = (q1.x + q2.x) * 0.5
+            Q.y = (q1.y + q2.y) * 0.5
+            Q.z = (q1.z + q2.z) * 0.5
+            return Q
+
+        ratio1 = math.sin((1 - t) * theta) / sintheta
+        ratio2 = math.sin(t * theta) / sintheta
+
+        Q.w = q1.w * ratio1 + q2.w * ratio2
+        Q.x = q1.x * ratio1 + q2.x * ratio2
+        Q.y = q1.y * ratio1 + q2.y * ratio2
+        Q.z = q1.z * ratio1 + q2.z * ratio2
+        return Q
+
+    new_interpolate = classmethod(new_interpolate)
+
+
+# Geometry
+# Much maths thanks to Paul Bourke, http://astronomy.swin.edu.au/~pbourke
+# ---------------------------------------------------------------------------
+
+class Geometry:
+    def _connect_unimplemented(self, other):
+        raise AttributeError('Cannot connect %s to %s' % \
+                             (self.__class__, other.__class__))
+
+    def _intersect_unimplemented(self, other):
+        raise AttributeError('Cannot intersect %s and %s' % \
+                             (self.__class__, other.__class__))
+
+    _intersect_point2 = _intersect_unimplemented
+    _intersect_line2 = _intersect_unimplemented
+    _intersect_circle = _intersect_unimplemented
+    _connect_point2 = _connect_unimplemented
+    _connect_line2 = _connect_unimplemented
+    _connect_circle = _connect_unimplemented
+
+    _intersect_point3 = _intersect_unimplemented
+    _intersect_line3 = _intersect_unimplemented
+    _intersect_sphere = _intersect_unimplemented
+    _intersect_plane = _intersect_unimplemented
+    _connect_point3 = _connect_unimplemented
+    _connect_line3 = _connect_unimplemented
+    _connect_sphere = _connect_unimplemented
+    _connect_plane = _connect_unimplemented
+
+    def intersect(self, other):
+        raise NotImplementedError
+
+    def connect(self, other):
+        raise NotImplementedError
+
+    def distance(self, other):
+        c = self.connect(other)
+        if c:
+            return c.length
+        return 0.0
+
+
+def _intersect_point2_circle(P, C):
+    return abs(P - C.c) <= C.r
+
+
+def _intersect_line2_line2(A, B):
+    d = B.v.y * A.v.x - B.v.x * A.v.y
+    if d == 0:
+        return None
+
+    dy = A.p.y - B.p.y
+    dx = A.p.x - B.p.x
+    ua = (B.v.x * dy - B.v.y * dx) / d
+    if not A._u_in(ua):
+        return None
+    ub = (A.v.x * dy - A.v.y * dx) / d
+    if not B._u_in(ub):
+        return None
+
+    return Point2(A.p.x + ua * A.v.x,
+                  A.p.y + ua * A.v.y)
+
+
+def _intersect_line2_circle(L, C):
+    a = L.v.magnitude_squared()
+    b = 2 * (L.v.x * (L.p.x - C.c.x) + \
+             L.v.y * (L.p.y - C.c.y))
+    c = C.c.magnitude_squared() + \
+        L.p.magnitude_squared() - \
+        2 * C.c.dot(L.p) - \
+        C.r ** 2
+    det = b ** 2 - 4 * a * c
+    if det < 0:
+        return None
+    sq = math.sqrt(det)
+    u1 = (-b + sq) / (2 * a)
+    u2 = (-b - sq) / (2 * a)
+    if not L._u_in(u1):
+        u1 = max(min(u1, 1.0), 0.0)
+    if not L._u_in(u2):
+        u2 = max(min(u2, 1.0), 0.0)
+    return LineSegment2(Point2(L.p.x + u1 * L.v.x,
+                               L.p.y + u1 * L.v.y),
+                        Point2(L.p.x + u2 * L.v.x,
+                               L.p.y + u2 * L.v.y))
+
+
+def _connect_point2_line2(P, L):
+    d = L.v.magnitude_squared()
+    assert d != 0
+    u = ((P.x - L.p.x) * L.v.x + \
+         (P.y - L.p.y) * L.v.y) / d
+    if not L._u_in(u):
+        u = max(min(u, 1.0), 0.0)
+    return LineSegment2(P,
+                        Point2(L.p.x + u * L.v.x,
+                               L.p.y + u * L.v.y))
+
+
+def _connect_point2_circle(P, C):
+    v = P - C.c
+    v.normalize()
+    v *= C.r
+    return LineSegment2(P, Point2(C.c.x + v.x, C.c.y + v.y))
+
+
+def _connect_line2_line2(A, B):
+    d = B.v.y * A.v.x - B.v.x * A.v.y
+    if d == 0:
+        # Parallel, connect an endpoint with a line
+        if isinstance(B, Ray2) or isinstance(B, LineSegment2):
+            p1, p2 = _connect_point2_line2(B.p, A)
+            return p2, p1
+        # No endpoint (or endpoint is on A), possibly choose arbitrary point
+        # on line.
+        return _connect_point2_line2(A.p, B)
+
+    dy = A.p.y - B.p.y
+    dx = A.p.x - B.p.x
+    ua = (B.v.x * dy - B.v.y * dx) / d
+    if not A._u_in(ua):
+        ua = max(min(ua, 1.0), 0.0)
+    ub = (A.v.x * dy - A.v.y * dx) / d
+    if not B._u_in(ub):
+        ub = max(min(ub, 1.0), 0.0)
+
+    return LineSegment2(Point2(A.p.x + ua * A.v.x, A.p.y + ua * A.v.y),
+                        Point2(B.p.x + ub * B.v.x, B.p.y + ub * B.v.y))
+
+
+def _connect_circle_line2(C, L):
+    d = L.v.magnitude_squared()
+    assert d != 0
+    u = ((C.c.x - L.p.x) * L.v.x + (C.c.y - L.p.y) * L.v.y) / d
+    if not L._u_in(u):
+        u = max(min(u, 1.0), 0.0)
+    point = Point2(L.p.x + u * L.v.x, L.p.y + u * L.v.y)
+    v = (point - C.c)
+    v.normalize()
+    v *= C.r
+    return LineSegment2(Point2(C.c.x + v.x, C.c.y + v.y), point)
+
+
+def _connect_circle_circle(A, B):
+    v = B.c - A.c
+    v.normalize()
+    return LineSegment2(Point2(A.c.x + v.x * A.r, A.c.y + v.y * A.r),
+                        Point2(B.c.x - v.x * B.r, B.c.y - v.y * B.r))
+
+
+class Point2(Vector2, Geometry):
+    def __repr__(self):
+        return 'Point2(%.2f, %.2f)' % (self.x, self.y)
+
+    def intersect(self, other):
+        return other._intersect_point2(self)
+
+    def _intersect_circle(self, other):
+        return _intersect_point2_circle(self, other)
+
+    def connect(self, other):
+        return other._connect_point2(self)
+
+    def _connect_point2(self, other):
+        return LineSegment2(other, self)
+
+    def _connect_line2(self, other):
+        c = _connect_point2_line2(self, other)
+        if c:
+            return c._swap()
+
+    def _connect_circle(self, other):
+        c = _connect_point2_circle(self, other)
+        if c:
+            return c._swap()
+
+
+class Line2(Geometry):
+    __slots__ = ['p', 'v']
+
+    def __init__(self, *args):
+        if len(args) == 3:
+            assert isinstance(args[0], Point2) and \
+                   isinstance(args[1], Vector2) and \
+                   type(args[2]) == float
+            self.p = args[0].copy()
+            self.v = args[1] * args[2] / abs(args[1])
+        elif len(args) == 2:
+            if isinstance(args[0], Point2) and isinstance(args[1], Point2):
+                self.p = args[0].copy()
+                self.v = args[1] - args[0]
+            elif isinstance(args[0], Point2) and isinstance(args[1], Vector2):
+                self.p = args[0].copy()
+                self.v = args[1].copy()
+            else:
+                raise AttributeError('%r' % (args,))
+        elif len(args) == 1:
+            if isinstance(args[0], Line2):
+                self.p = args[0].p.copy()
+                self.v = args[0].v.copy()
+            else:
+                raise AttributeError('%r' % (args,))
+        else:
+            raise AttributeError('%r' % (args,))
+
+        if not self.v:
+            raise AttributeError('Line has zero-length vector')
+
+    def __copy__(self):
+        return self.__class__(self.p, self.v)
+
+    copy = __copy__
+
+    def __repr__(self):
+        return 'Line2(<%.2f, %.2f> + u<%.2f, %.2f>)' % \
+               (self.p.x, self.p.y, self.v.x, self.v.y)
+
+    p1 = property(lambda self: self.p)
+    p2 = property(lambda self: Point2(self.p.x + self.v.x,
+                                      self.p.y + self.v.y))
+
+    def _apply_transform(self, t):
+        self.p = t * self.p
+        self.v = t * self.v
+
+    def _u_in(self, u):
+        return True
+
+    def intersect(self, other):
+        return other._intersect_line2(self)
+
+    def _intersect_line2(self, other):
+        return _intersect_line2_line2(self, other)
+
+    def _intersect_circle(self, other):
+        return _intersect_line2_circle(self, other)
+
+    def connect(self, other):
+        return other._connect_line2(self)
+
+    def _connect_point2(self, other):
+        return _connect_point2_line2(other, self)
+
+    def _connect_line2(self, other):
+        return _connect_line2_line2(other, self)
+
+    def _connect_circle(self, other):
+        return _connect_circle_line2(other, self)
+
+
+class Ray2(Line2):
+    def __repr__(self):
+        return 'Ray2(<%.2f, %.2f> + u<%.2f, %.2f>)' % \
+               (self.p.x, self.p.y, self.v.x, self.v.y)
+
+    def _u_in(self, u):
+        return u >= 0.0
+
+
+class LineSegment2(Line2):
+    def __repr__(self):
+        return 'LineSegment2(<%.2f, %.2f> to <%.2f, %.2f>)' % \
+               (self.p.x, self.p.y, self.p.x + self.v.x, self.p.y + self.v.y)
+
+    def _u_in(self, u):
+        return u >= 0.0 and u <= 1.0
+
+    def __abs__(self):
+        return abs(self.v)
+
+    def magnitude_squared(self):
+        return self.v.magnitude_squared()
+
+    def _swap(self):
+        # used by connect methods to switch order of points
+        self.p = self.p2
+        self.v *= -1
+        return self
+
+    length = property(lambda self: abs(self.v))
+
+
+class Circle(Geometry):
+    __slots__ = ['c', 'r']
+
+    def __init__(self, center, radius):
+        assert isinstance(center, Vector2) and type(radius) == float
+        self.c = center.copy()
+        self.r = radius
+
+    def __copy__(self):
+        return self.__class__(self.c, self.r)
+
+    copy = __copy__
+
+    def __repr__(self):
+        return 'Circle(<%.2f, %.2f>, radius=%.2f)' % \
+               (self.c.x, self.c.y, self.r)
+
+    def _apply_transform(self, t):
+        self.c = t * self.c
+
+    def intersect(self, other):
+        return other._intersect_circle(self)
+
+    def _intersect_point2(self, other):
+        return _intersect_point2_circle(other, self)
+
+    def _intersect_line2(self, other):
+        return _intersect_line2_circle(other, self)
+
+    def connect(self, other):
+        return other._connect_circle(self)
+
+    def _connect_point2(self, other):
+        return _connect_point2_circle(other, self)
+
+    def _connect_line2(self, other):
+        c = _connect_circle_line2(self, other)
+        if c:
+            return c._swap()
+
+    def _connect_circle(self, other):
+        return _connect_circle_circle(other, self)
+
+
+# 3D Geometry
+# -------------------------------------------------------------------------
+
+def _connect_point3_line3(P, L):
+    d = L.v.magnitude_squared()
+    assert d != 0
+    u = ((P.x - L.p.x) * L.v.x + \
+         (P.y - L.p.y) * L.v.y + \
+         (P.z - L.p.z) * L.v.z) / d
+    if not L._u_in(u):
+        u = max(min(u, 1.0), 0.0)
+    return LineSegment3(P, Point3(L.p.x + u * L.v.x,
+                                  L.p.y + u * L.v.y,
+                                  L.p.z + u * L.v.z))
+
+
+def _connect_point3_sphere(P, S):
+    v = P - S.c
+    v.normalize()
+    v *= S.r
+    return LineSegment3(P, Point3(S.c.x + v.x, S.c.y + v.y, S.c.z + v.z))
+
+
+def _connect_point3_plane(p, plane):
+    n = plane.n.normalized()
+    d = p.dot(plane.n) - plane.k
+    return LineSegment3(p, Point3(p.x - n.x * d, p.y - n.y * d, p.z - n.z * d))
+
+
+def _connect_line3_line3(A, B):
+    assert A.v and B.v
+    p13 = A.p - B.p
+    d1343 = p13.dot(B.v)
+    d4321 = B.v.dot(A.v)
+    d1321 = p13.dot(A.v)
+    d4343 = B.v.magnitude_squared()
+    denom = A.v.magnitude_squared() * d4343 - d4321 ** 2
+    if denom == 0:
+        # Parallel, connect an endpoint with a line
+        if isinstance(B, Ray3) or isinstance(B, LineSegment3):
+            return _connect_point3_line3(B.p, A)._swap()
+        # No endpoint (or endpoint is on A), possibly choose arbitrary
+        # point on line.
+        return _connect_point3_line3(A.p, B)
+
+    ua = (d1343 * d4321 - d1321 * d4343) / denom
+    if not A._u_in(ua):
+        ua = max(min(ua, 1.0), 0.0)
+    ub = (d1343 + d4321 * ua) / d4343
+    if not B._u_in(ub):
+        ub = max(min(ub, 1.0), 0.0)
+    return LineSegment3(Point3(A.p.x + ua * A.v.x,
+                               A.p.y + ua * A.v.y,
+                               A.p.z + ua * A.v.z),
+                        Point3(B.p.x + ub * B.v.x,
+                               B.p.y + ub * B.v.y,
+                               B.p.z + ub * B.v.z))
+
+
+def _connect_line3_plane(L, P):
+    d = P.n.dot(L.v)
+    if not d:
+        # Parallel, choose an endpoint
+        return _connect_point3_plane(L.p, P)
+    u = (P.k - P.n.dot(L.p)) / d
+    if not L._u_in(u):
+        # intersects out of range, choose nearest endpoint
+        u = max(min(u, 1.0), 0.0)
+        return _connect_point3_plane(Point3(L.p.x + u * L.v.x,
+                                            L.p.y + u * L.v.y,
+                                            L.p.z + u * L.v.z), P)
+    # Intersection
+    return None
+
+
+def _connect_sphere_line3(S, L):
+    d = L.v.magnitude_squared()
+    assert d != 0
+    u = ((S.c.x - L.p.x) * L.v.x + \
+         (S.c.y - L.p.y) * L.v.y + \
+         (S.c.z - L.p.z) * L.v.z) / d
+    if not L._u_in(u):
+        u = max(min(u, 1.0), 0.0)
+    point = Point3(L.p.x + u * L.v.x, L.p.y + u * L.v.y, L.p.z + u * L.v.z)
+    v = (point - S.c)
+    v.normalize()
+    v *= S.r
+    return LineSegment3(Point3(S.c.x + v.x, S.c.y + v.y, S.c.z + v.z),
+                        point)
+
+
+def _connect_sphere_sphere(A, B):
+    v = B.c - A.c
+    v.normalize()
+    return LineSegment3(Point3(A.c.x + v.x * A.r,
+                               A.c.y + v.y * A.r,
+                               A.c.x + v.z * A.r),
+                        Point3(B.c.x + v.x * B.r,
+                               B.c.y + v.y * B.r,
+                               B.c.x + v.z * B.r))
+
+
+def _connect_sphere_plane(S, P):
+    c = _connect_point3_plane(S.c, P)
+    if not c:
+        return None
+    p2 = c.p2
+    v = p2 - S.c
+    v.normalize()
+    v *= S.r
+    return LineSegment3(Point3(S.c.x + v.x, S.c.y + v.y, S.c.z + v.z),
+                        p2)
+
+
+def _connect_plane_plane(A, B):
+    if A.n.cross(B.n):
+        # Planes intersect
+        return None
+    else:
+        # Planes are parallel, connect to arbitrary point
+        return _connect_point3_plane(A._get_point(), B)
+
+
+def _intersect_point3_sphere(P, S):
+    return abs(P - S.c) <= S.r
+
+
+def _intersect_line3_sphere(L, S):
+    a = L.v.magnitude_squared()
+    b = 2 * (L.v.x * (L.p.x - S.c.x) + \
+             L.v.y * (L.p.y - S.c.y) + \
+             L.v.z * (L.p.z - S.c.z))
+    c = S.c.magnitude_squared() + \
+        L.p.magnitude_squared() - \
+        2 * S.c.dot(L.p) - \
+        S.r ** 2
+    det = b ** 2 - 4 * a * c
+    if det < 0:
+        return None
+    sq = math.sqrt(det)
+    u1 = (-b + sq) / (2 * a)
+    u2 = (-b - sq) / (2 * a)
+    if not L._u_in(u1):
+        u1 = max(min(u1, 1.0), 0.0)
+    if not L._u_in(u2):
+        u2 = max(min(u2, 1.0), 0.0)
+    return LineSegment3(Point3(L.p.x + u1 * L.v.x,
+                               L.p.y + u1 * L.v.y,
+                               L.p.z + u1 * L.v.z),
+                        Point3(L.p.x + u2 * L.v.x,
+                               L.p.y + u2 * L.v.y,
+                               L.p.z + u2 * L.v.z))
+
+
+def _intersect_line3_plane(L, P):
+    d = P.n.dot(L.v)
+    if not d:
+        # Parallel
+        return None
+    u = (P.k - P.n.dot(L.p)) / d
+    if not L._u_in(u):
+        return None
+    return Point3(L.p.x + u * L.v.x,
+                  L.p.y + u * L.v.y,
+                  L.p.z + u * L.v.z)
+
+
+def _intersect_plane_plane(A, B):
+    n1_m = A.n.magnitude_squared()
+    n2_m = B.n.magnitude_squared()
+    n1d2 = A.n.dot(B.n)
+    det = n1_m * n2_m - n1d2 ** 2
+    if det == 0:
+        # Parallel
+        return None
+    c1 = (A.k * n2_m - B.k * n1d2) / det
+    c2 = (B.k * n1_m - A.k * n1d2) / det
+    return Line3(Point3(c1 * A.n.x + c2 * B.n.x,
+                        c1 * A.n.y + c2 * B.n.y,
+                        c1 * A.n.z + c2 * B.n.z),
+                 A.n.cross(B.n))
+
+
+class Point3(Vector3, Geometry):
+    def __repr__(self):
+        return 'Point3(%.2f, %.2f, %.2f)' % (self.x, self.y, self.z)
+
+    def intersect(self, other):
+        return other._intersect_point3(self)
+
+    def _intersect_sphere(self, other):
+        return _intersect_point3_sphere(self, other)
+
+    def connect(self, other):
+        return other._connect_point3(self)
+
+    def _connect_point3(self, other):
+        if self != other:
+            return LineSegment3(other, self)
+        return None
+
+    def _connect_line3(self, other):
+        c = _connect_point3_line3(self, other)
+        if c:
+            return c._swap()
+
+    def _connect_sphere(self, other):
+        c = _connect_point3_sphere(self, other)
+        if c:
+            return c._swap()
+
+    def _connect_plane(self, other):
+        c = _connect_point3_plane(self, other)
+        if c:
+            return c._swap()
+
+
+class Line3:
+    __slots__ = ['p', 'v']
+
+    def __init__(self, *args):
+        if len(args) == 3:
+            assert isinstance(args[0], Point3) and \
+                   isinstance(args[1], Vector3) and \
+                   type(args[2]) == float
+            self.p = args[0].copy()
+            self.v = args[1] * args[2] / abs(args[1])
+        elif len(args) == 2:
+            if isinstance(args[0], Point3) and isinstance(args[1], Point3):
+                self.p = args[0].copy()
+                self.v = args[1] - args[0]
+            elif isinstance(args[0], Point3) and isinstance(args[1], Vector3):
+                self.p = args[0].copy()
+                self.v = args[1].copy()
+            else:
+                raise AttributeError('%r' % (args,))
+        elif len(args) == 1:
+            if isinstance(args[0], Line3):
+                self.p = args[0].p.copy()
+                self.v = args[0].v.copy()
+            else:
+                raise AttributeError('%r' % (args,))
+        else:
+            raise AttributeError('%r' % (args,))
+
+            # XXX This is annoying.
+            # if not self.v:
+            #    raise AttributeError, 'Line has zero-length vector'
+
+    def __copy__(self):
+        return self.__class__(self.p, self.v)
+
+    copy = __copy__
+
+    def __repr__(self):
+        return 'Line3(<%.2f, %.2f, %.2f> + u<%.2f, %.2f, %.2f>)' % \
+               (self.p.x, self.p.y, self.p.z, self.v.x, self.v.y, self.v.z)
+
+    p1 = property(lambda self: self.p)
+    p2 = property(lambda self: Point3(self.p.x + self.v.x,
+                                      self.p.y + self.v.y,
+                                      self.p.z + self.v.z))
+
+    def _apply_transform(self, t):
+        self.p = t * self.p
+        self.v = t * self.v
+
+    def _u_in(self, u):
+        return True
+
+    def intersect(self, other):
+        return other._intersect_line3(self)
+
+    def _intersect_sphere(self, other):
+        return _intersect_line3_sphere(self, other)
+
+    def _intersect_plane(self, other):
+        return _intersect_line3_plane(self, other)
+
+    def connect(self, other):
+        return other._connect_line3(self)
+
+    def _connect_point3(self, other):
+        return _connect_point3_line3(other, self)
+
+    def _connect_line3(self, other):
+        return _connect_line3_line3(other, self)
+
+    def _connect_sphere(self, other):
+        return _connect_sphere_line3(other, self)
+
+    def _connect_plane(self, other):
+        c = _connect_line3_plane(self, other)
+        if c:
+            return c
+
+
+class Ray3(Line3):
+    def __repr__(self):
+        return 'Ray3(<%.2f, %.2f, %.2f> + u<%.2f, %.2f, %.2f>)' % \
+               (self.p.x, self.p.y, self.p.z, self.v.x, self.v.y, self.v.z)
+
+    def _u_in(self, u):
+        return u >= 0.0
+
+
+class LineSegment3(Line3):
+    def __repr__(self):
+        return 'LineSegment3(<%.2f, %.2f, %.2f> to <%.2f, %.2f, %.2f>)' % \
+               (self.p.x, self.p.y, self.p.z,
+                self.p.x + self.v.x, self.p.y + self.v.y, self.p.z + self.v.z)
+
+    def _u_in(self, u):
+        return u >= 0.0 and u <= 1.0
+
+    def __abs__(self):
+        return abs(self.v)
+
+    def magnitude_squared(self):
+        return self.v.magnitude_squared()
+
+    def _swap(self):
+        # used by connect methods to switch order of points
+        self.p = self.p2
+        self.v *= -1
+        return self
+
+    length = property(lambda self: abs(self.v))
+
+
+class Sphere:
+    __slots__ = ['c', 'r']
+
+    def __init__(self, center, radius):
+        assert isinstance(center, Vector3) and type(radius) == float
+        self.c = center.copy()
+        self.r = radius
+
+    def __copy__(self):
+        return self.__class__(self.c, self.r)
+
+    copy = __copy__
+
+    def __repr__(self):
+        return 'Sphere(<%.2f, %.2f, %.2f>, radius=%.2f)' % \
+               (self.c.x, self.c.y, self.c.z, self.r)
+
+    def _apply_transform(self, t):
+        self.c = t * self.c
+
+    def intersect(self, other):
+        return other._intersect_sphere(self)
+
+    def _intersect_point3(self, other):
+        return _intersect_point3_sphere(other, self)
+
+    def _intersect_line3(self, other):
+        return _intersect_line3_sphere(other, self)
+
+    def connect(self, other):
+        return other._connect_sphere(self)
+
+    def _connect_point3(self, other):
+        return _connect_point3_sphere(other, self)
+
+    def _connect_line3(self, other):
+        c = _connect_sphere_line3(self, other)
+        if c:
+            return c._swap()
+
+    def _connect_sphere(self, other):
+        return _connect_sphere_sphere(other, self)
+
+    def _connect_plane(self, other):
+        c = _connect_sphere_plane(self, other)
+        if c:
+            return c
+
+
+class Plane:
+    # n.p = k, where n is normal, p is point on plane, k is constant scalar
+    __slots__ = ['n', 'k']
+
+    def __init__(self, *args):
+        if len(args) == 3:
+            assert isinstance(args[0], Point3) and \
+                   isinstance(args[1], Point3) and \
+                   isinstance(args[2], Point3)
+            self.n = (args[1] - args[0]).cross(args[2] - args[0])
+            self.n.normalize()
+            self.k = self.n.dot(args[0])
+        elif len(args) == 2:
+            if isinstance(args[0], Point3) and isinstance(args[1], Vector3):
+                self.n = args[1].normalized()
+                self.k = self.n.dot(args[0])
+            elif isinstance(args[0], Vector3) and type(args[1]) == float:
+                self.n = args[0].normalized()
+                self.k = args[1]
+            else:
+                raise AttributeError('%r' % (args,))
+
+        else:
+            raise AttributeError('%r' % (args,))
+
+        if not self.n:
+            raise AttributeError('Points on plane are colinear')
+
+    def __copy__(self):
+        return self.__class__(self.n, self.k)
+
+    copy = __copy__
+
+    def __repr__(self):
+        return 'Plane(<%.2f, %.2f, %.2f>.p = %.2f)' % \
+               (self.n.x, self.n.y, self.n.z, self.k)
+
+    def _get_point(self):
+        # Return an arbitrary point on the plane
+        if self.n.z:
+            return Point3(0., 0., self.k / self.n.z)
+        elif self.n.y:
+            return Point3(0., self.k / self.n.y, 0.)
+        else:
+            return Point3(self.k / self.n.x, 0., 0.)
+
+    def _apply_transform(self, t):
+        p = t * self._get_point()
+        self.n = t * self.n
+        self.k = self.n.dot(p)
+
+    def intersect(self, other):
+        return other._intersect_plane(self)
+
+    def _intersect_line3(self, other):
+        return _intersect_line3_plane(other, self)
+
+    def _intersect_plane(self, other):
+        return _intersect_plane_plane(self, other)
+
+    def connect(self, other):
+        return other._connect_plane(self)
+
+    def _connect_point3(self, other):
+        return _connect_point3_plane(other, self)
+
+    def _connect_line3(self, other):
+        return _connect_line3_plane(other, self)
+
+    def _connect_sphere(self, other):
+        return _connect_sphere_plane(other, self)
+
+    def _connect_plane(self, other):
+        return _connect_plane_plane(other, self)
diff --git a/contrib/toys/follow_mouse.py b/contrib/toys/follow_mouse.py
new file mode 100644
index 0000000..f8f48af
--- /dev/null
+++ b/contrib/toys/follow_mouse.py
@@ -0,0 +1,101 @@
+'''
+Code by Richard Jones, released into the public domain.
+
+Inspired by http://screamyguy.net/lines/index.htm
+
+This code uses a single drawing buffer so that successive drawing passes
+may be used to create the line fading effect. The fading is achieved
+by drawing a translucent black quad over the entire scene before drawing
+the next line segment.
+
+Note: when working with a single buffer it is always a good idea to
+glFlush() when you've finished your rendering.
+'''
+
+
+import sys
+import random
+
+import pyglet
+from pyglet.gl import *
+
+# open a single-buffered window so we can do cheap accumulation
+config = Config(double_buffer=False)
+window = pyglet.window.Window(fullscreen='-fs' in sys.argv, config=config)
+
+class Line:
+    batch = pyglet.graphics.Batch()
+    lines = batch.add(100, GL_LINES, None, ('v2f', (0.0,)  * 200), ('c4B', (255, ) * 400))
+    unallocated = list(range(100))
+    active = []
+
+    def __init__(self):
+        self.n = self.unallocated.pop()
+        self.active.append(self)
+        self.x = self.lx = random.randint(0, window.width)
+        self.y = self.ly = random.randint(0, window.height)
+        self.dx = random.randint(-70, 70)
+        self.dy = random.randint(-70, 70)
+        self.damping = random.random() * .15 + .8
+        self.power = random.random() * .1 + .05
+
+    def update(self, dt):
+        # calculate my acceleration based on the distance to the mouse
+        # pointer and my acceleration power
+        dx2 = (self.x - self.mouse_x) / self.power
+        dy2 = (self.y - self.mouse_y) / self.power
+
+        # now figure my new velocity
+        self.dx -= dx2 * dt
+        self.dy -= dy2 * dt
+        self.dx *= self.damping
+        self.dy *= self.damping
+
+        # calculate new line endpoints
+        self.lx = self.x
+        self.ly = self.y
+        self.x += self.dx * dt
+        self.y += self.dy * dt
+
+    @classmethod
+    def on_draw(cls):
+        # darken the existing display a little
+        glColor4f(0, 0, 0, .05)
+        glRectf(0, 0, window.width, window.height)
+
+        # render the new lines
+        cls.batch.draw()
+
+        glFlush()
+
+    mouse_x = mouse_y = 0
+    @classmethod
+    def on_mouse_motion(cls, x, y, dx, dy):
+        cls.mouse_x, cls.mouse_y = x, y
+
+    @classmethod
+    def tick(cls, dt):
+        if len(cls.active) < 50 and random.random() < .1:
+            cls()
+
+        if len(cls.active) > 10 and random.random() < .01:
+            line = cls.active.pop(0)
+            cls.unallocated.append(line.n)
+
+        # update line positions
+        n = len(cls.active)
+        for n, line in enumerate(cls.active):
+            line.update(dt)
+            cls.lines.vertices[n*4:n*4+4] = [line.lx, line.ly, line.x, line.y]
+
+glEnable(GL_BLEND)
+glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+glEnable(GL_LINE_SMOOTH)
+
+# need to set a FPS limit since we're not going to be limited by VSYNC in single-buffer mode
+pyglet.clock.set_fps_limit(30)
+
+pyglet.clock.schedule(Line.tick)
+window.push_handlers(Line)
+pyglet.app.run()
+
diff --git a/contrib/toys/primitives.py b/contrib/toys/primitives.py
new file mode 100644
index 0000000..199c32c
--- /dev/null
+++ b/contrib/toys/primitives.py
@@ -0,0 +1,40 @@
+import math
+
+import pyglet
+from pyglet.gl import *
+
+class SmoothLineGroup(pyglet.graphics.Group):
+    def set_state(self):
+        glPushAttrib(GL_ENABLE_BIT)
+        glEnable(GL_BLEND)
+        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+        glEnable(GL_LINE_SMOOTH)
+        glLineWidth(2)
+
+    def unset_state(self):
+        glPopAttrib()
+
+    def __hash__(self):
+        return hash(self.__class__.__name__)
+
+    def __eq__(self, other):
+        return self.__class__ is other.__class__
+
+def add_circle(batch, x, y, radius, color, num_points=20, antialised=True):
+    l = []
+    for n in range(num_points):
+        angle = (math.pi * 2 * n) / num_points
+        l.append(int(x + radius * math.cos(angle)))
+        l.append(int(y + radius * math.sin(angle)))
+    l.append(int(x + radius * 1))
+    l.append(int(y))
+    num_points += 3
+    l[0:0] = l[0:2]
+    l.extend(l[-2:])
+    if antialised:
+        group = SmoothLineGroup()
+    else:
+        group = None
+    return batch.add(num_points, GL_LINE_STRIP, group, ('v2i', l),
+        ('c4B', color*num_points))
+
diff --git a/contrib/toys/thrust.py b/contrib/toys/thrust.py
new file mode 100644
index 0000000..2881c10
--- /dev/null
+++ b/contrib/toys/thrust.py
@@ -0,0 +1,168 @@
+'''
+Code by Richard Jones, released into the public domain.
+
+Beginnings of something like http://en.wikipedia.org/wiki/Thrust_(video_game)
+'''
+import sys
+import math
+
+import euclid
+
+import primitives
+
+import pyglet
+from pyglet.window import key
+from pyglet.gl import *
+
+window = pyglet.window.Window(fullscreen='-fs' in sys.argv)
+
+GRAVITY = -200
+
+class Game:
+    def __init__(self):
+        self.batch = pyglet.graphics.Batch()
+        self.ship = Ship(window.width//2, window.height//2, self.batch)
+        self.debug_text = pyglet.text.Label('debug text', x=10, y=window.height-40, batch=self.batch)
+
+    def on_draw(self):
+        window.clear()
+        self.batch.draw()
+
+    def update(self, dt):
+        self.ship.update(dt)
+
+
+class Ship:
+    def __init__(self, x, y, batch):
+        self.position = euclid.Point2(x, y)
+        self.velocity = euclid.Point2(0, 0)
+        self.angle = math.pi/2
+
+        self.batch = batch
+        self.lines = batch.add(6, GL_LINES, primitives.SmoothLineGroup(),
+            ('v2f', (0, 0) * 6),
+            ('c4B', (255, 255, 255, 255) * 6))
+
+        self.ball_position = euclid.Point2(window.width/2, window.height/4)
+        self.ball_velocity = euclid.Point2(0, 0)
+        self.ball_lines = primitives.add_circle(batch, 0, 0, 20, (255, 255, 255, 255), 20)
+        self._ball_verts = list(self.ball_lines.vertices)
+        self._update_ball_verts()
+
+        self.join_active = False
+        self.join_line = None
+
+        self.joined = False
+
+    def update(self, dt):
+        self.angle += (keyboard[key.LEFT] - keyboard[key.RIGHT]) * math.pi * dt
+        r = euclid.Matrix3.new_rotate(self.angle)
+        if keyboard[key.UP]:
+            thrust = r * euclid.Vector2(600, 0)
+        else:
+            thrust = euclid.Vector2(0, 0)
+
+        # attempt join on spacebar press
+        s_b = self.position - self.ball_position
+        if keyboard[key.SPACE] and abs(s_b) < 100:
+            self.join_active = True
+
+        if not self.joined:
+            # simulation is just the ship
+
+            # apply thrust to the ship directly
+            thrust.y += GRAVITY
+
+            # now figure my new velocity
+            self.velocity += thrust * dt
+
+            # calculate new line endpoints
+            self.position += self.velocity * dt
+
+        else:
+            # simulation is of a rod with ship and one end and ball at other
+            n_v = s_b.normalized()
+            n_t = thrust.normalized()
+
+            # figure the linear acceleration, velocity & move
+            d = abs(n_v.dot(n_t))
+            lin = thrust * d
+            lin.y += GRAVITY
+            self.velocity += lin * dt
+            self.cog += self.velocity * dt
+
+            # now the angular acceleration
+            r90 = euclid.Matrix3.new_rotate(math.pi/2)
+            r_n_t = r90 * n_t
+            rd = n_v.dot(r_n_t)
+            self.ang_velocity -= abs(abs(thrust)) * rd * 0.0001
+            self.join_angle += self.ang_velocity * dt
+
+            # vector from center of gravity our to either end
+            ar = euclid.Matrix3.new_rotate(self.join_angle)
+            a_r = ar * euclid.Vector2(self.join_length/2, 0)
+
+            # set the ship & ball positions
+            self.position = self.cog + a_r
+            self.ball_position = self.cog - a_r
+
+            self._update_ball_verts()
+
+        if self.join_active:
+            if abs(s_b) >= 100 and not self.joined:
+                self.joined = True
+                h_s_b = s_b / 2
+                self.cog = self.position - h_s_b
+                self.join_angle = math.atan2(s_b.y, s_b.x)
+                self.join_length = abs(s_b)
+
+                # mass just doubled, so slow linear velocity down
+                self.velocity /= 2
+
+                # XXX and generate some initial angular velocity based on
+                # XXX ship current velocity
+                self.ang_velocity = 0
+
+            # render the join line
+            l = [
+                self.position.x, self.position.y,
+                self.ball_position.x, self.ball_position.y
+            ]
+            if self.join_line:
+                self.join_line.vertices[:] = l
+            else:
+                self.join_line = self.batch.add(2, GL_LINES, primitives.SmoothLineGroup(),
+                    ('v2f', l), ('c4B', (255, 255, 255, 255) * 2))
+
+        # update the ship verts
+        bl = r * euclid.Point2(-25, 25)
+        t = r * euclid.Point2(25, 0)
+        br = r * euclid.Point2(-25, -25)
+        x, y = self.position
+        self.lines.vertices[:] = [
+            x+bl.x, y+bl.y, x+t.x, y+t.y,
+            x+t.x, y+t.y, x+br.x, y+br.y,
+            x+br.x, y+br.y, x+bl.x, y+bl.y,
+        ]
+
+    def _update_ball_verts(self):
+        # update the ball for its position
+        l = []
+        x, y = self.ball_position
+        for i, v in enumerate(self._ball_verts):
+            if i % 2:
+                l.append(int(v + y))
+            else:
+                l.append(int(v + x))
+        self.ball_lines.vertices[:] = l
+
+g = Game()
+
+window.push_handlers(g)
+pyglet.clock.schedule(g.update)
+
+keyboard = key.KeyStateHandler()
+window.push_handlers(keyboard)
+
+pyglet.app.run()
+
diff --git a/debian/changelog b/debian/changelog
index 10249ab..2ee9427 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+pyglet (1.5.24-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sun, 15 May 2022 11:44:33 -0000
+
 pyglet (1.5.14-2) unstable; urgency=medium
 
   * Team upload.
diff --git a/debian/patches/0004-Don-t-ship-copy-of-extlibs-in-the-binary-packages.patch b/debian/patches/0004-Don-t-ship-copy-of-extlibs-in-the-binary-packages.patch
index 12320fd..b081fa8 100644
--- a/debian/patches/0004-Don-t-ship-copy-of-extlibs-in-the-binary-packages.patch
+++ b/debian/patches/0004-Don-t-ship-copy-of-extlibs-in-the-binary-packages.patch
@@ -8,11 +8,13 @@ Signed-off-by: Mark Hymers <mhy@debian.org>
  setup.py                   | 3 +--
  2 files changed, 4 insertions(+), 3 deletions(-)
 
---- a/pyglet/image/codecs/png.py
-+++ b/pyglet/image/codecs/png.py
+Index: pyglet/pyglet/image/codecs/png.py
+===================================================================
+--- pyglet.orig/pyglet/image/codecs/png.py
++++ pyglet/pyglet/image/codecs/png.py
 @@ -42,7 +42,7 @@ import itertools
- from pyglet.image import *
- from pyglet.image.codecs import *
+ from pyglet.image import ImageData, ImageDecodeException
+ from pyglet.image.codecs import ImageDecoder, ImageEncoder
  
 -import pyglet.extlibs.png as pypng
 +import png as pypng
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 0000000..a05c041
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,154 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = _build
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html       to make standalone HTML files"
+	@echo "  dirhtml    to make HTML files named index.html in directories"
+	@echo "  singlehtml to make a single large HTML file"
+	@echo "  pickle     to make pickle files"
+	@echo "  json       to make JSON files"
+	@echo "  htmlhelp   to make HTML files and a HTML help project"
+	@echo "  qthelp     to make HTML files and a qthelp project"
+	@echo "  devhelp    to make HTML files and a Devhelp project"
+	@echo "  epub       to make an epub"
+	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
+	@echo "  text       to make text files"
+	@echo "  man        to make manual pages"
+	@echo "  texinfo    to make Texinfo files"
+	@echo "  info       to make Texinfo files and run them through makeinfo"
+	@echo "  gettext    to make PO message catalogs"
+	@echo "  changes    to make an overview of all changed/added/deprecated items"
+	@echo "  linkcheck  to check all external links for integrity"
+	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+	-rm -rf $(BUILDDIR)/*
+
+html:
+	mkdir -p _build/html
+	$(SPHINXBUILD) -w $(BUILDDIR)/html/warnings.txt -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+	@echo
+	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Pyglet.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Pyglet.qhc"
+
+devhelp:
+	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+	@echo
+	@echo "Build finished."
+	@echo "To view the help file:"
+	@echo "# mkdir -p $$HOME/.local/share/devhelp/Pyglet"
+	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Pyglet"
+	@echo "# devhelp"
+
+epub:
+	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+	@echo
+	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make' in that directory to run these through (pdf)latex" \
+	      "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through pdflatex..."
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+	@echo
+	@echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+	@echo
+	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo
+	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+	@echo "Run \`make' in that directory to run these through makeinfo" \
+	      "(use \`make info' here to do that automatically)."
+
+info:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo "Running Texinfo files through makeinfo..."
+	make -C $(BUILDDIR)/texinfo info
+	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+	@echo
+	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/doc/README.md b/doc/README.md
new file mode 100644
index 0000000..d26a449
--- /dev/null
+++ b/doc/README.md
@@ -0,0 +1,48 @@
+# Building the documentation
+
+## Basic
+
+Install doc requirements:
+
+    pip install -r doc/requirements.txt
+
+The simpleast way build html docs:
+
+    python setup.py build_sphinx
+
+The build result will appear in a `_build` directory in the root of the project.
+
+## Advanced
+
+The more advanced way to building docs is using `make`
+(`make.bat` for windows).
+
+    cd doc/
+    make html
+
+The HTML docs will be generated in the `doc/_build` subdirectory.
+
+Please run `make help` for a complete list of targets, but be aware that some
+of them may have extra requirements.
+
+    $ make help
+    Please use `make <target>' where <target> is one of
+    html       to make standalone HTML files
+    dirhtml    to make HTML files named index.html in directories
+    singlehtml to make a single large HTML file
+    pickle     to make pickle files
+    json       to make JSON files
+    htmlhelp   to make HTML files and a HTML help project
+    qthelp     to make HTML files and a qthelp project
+    devhelp    to make HTML files and a Devhelp project
+    epub       to make an epub
+    latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+    latexpdf   to make LaTeX files and run them through pdflatex
+    text       to make text files
+    man        to make manual pages
+    texinfo    to make Texinfo files
+    info       to make Texinfo files and run them through makeinfo
+    gettext    to make PO message catalogs
+    changes    to make an overview of all changed/added/deprecated items
+    linkcheck  to check all external links for integrity
+    doctest    to run all doctests embedded in the documentation (if enabled)
diff --git a/doc/_static/favicon.ico b/doc/_static/favicon.ico
new file mode 100644
index 0000000..ca5fcf0
Binary files /dev/null and b/doc/_static/favicon.ico differ
diff --git a/doc/_static/logo.png b/doc/_static/logo.png
new file mode 100644
index 0000000..075726d
Binary files /dev/null and b/doc/_static/logo.png differ
diff --git a/doc/_static/logo5.svg b/doc/_static/logo5.svg
new file mode 100644
index 0000000..fde4375
--- /dev/null
+++ b/doc/_static/logo5.svg
@@ -0,0 +1,278 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="744.09448819"
+   height="1052.3622047"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.44.1"
+   sodipodi:docbase="/home/alex/projects/pyglet/doc"
+   sodipodi:docname="logo5.svg">
+  <defs
+     id="defs4">
+    <marker
+       inkscape:stockid="TriangleOutS"
+       orient="auto"
+       refY="0.0"
+       refX="0.0"
+       id="TriangleOutS"
+       style="overflow:visible">
+      <path
+         id="path2902"
+         d="M 5.77,0.0 L -2.88,5.0 L -2.88,-5.0 L 5.77,0.0 z "
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt;marker-start:none"
+         transform="scale(0.2)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow2Send"
+       orient="auto"
+       refY="0.0"
+       refX="0.0"
+       id="Arrow2Send"
+       style="overflow:visible;">
+      <path
+         id="path2973"
+         style="font-size:12.0;fill-rule:evenodd;stroke-width:0.62500000;stroke-linejoin:round;"
+         d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
+         transform="scale(0.3) rotate(180) translate(-2.3,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0.0"
+       refX="0.0"
+       id="Arrow1Lend"
+       style="overflow:visible;">
+      <path
+         id="path3003"
+         d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt;marker-start:none;"
+         transform="scale(0.8) rotate(180) translate(12.5,0)" />
+    </marker>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.7"
+     inkscape:cx="576.43876"
+     inkscape:cy="530.06977"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     inkscape:window-width="1230"
+     inkscape:window-height="972"
+     inkscape:window-x="1280"
+     inkscape:window-y="0" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     style="display:inline">
+    <path
+       id="path3874"
+       d="M 558.39286,90.116074 C 493.4914,90.116074 440.64286,142.96462 440.64286,207.86607 C 440.64285,272.76752 493.49141,325.61608 558.39286,325.61607 C 623.27328,325.61607 676.10866,272.80119 676.14286,207.92857 C 676.14286,207.91806 676.14286,207.90783 676.14286,207.89732 C 676.14287,207.8869 676.14288,207.87649 676.14286,207.86607 C 676.14895,207.67862 676.14896,207.49102 676.14286,207.30357 C 676.14234,207.19801 676.1124,207.09657 676.11161,206.99107 C 675.62946,142.439 622.98559,90.116084 558.39286,90.116074 z M 558.39286,107.42857 C 613.76734,107.42858 658.56335,151.99222 658.83036,207.36607 C 658.82847,207.47023 658.82848,207.57441 658.83036,207.67857 C 658.82967,207.74107 658.82968,207.80357 658.83036,207.86607 C 658.83036,263.40713 613.93392,308.30357 558.39286,308.30357 C 502.85179,308.30358 457.95536,263.40714 457.95536,207.86607 C 457.95535,165.65019 483.90148,129.58559 520.73661,114.70982 C 513.30382,131.33866 510.56347,149.77322 513.42411,166.58482 C 514.23061,171.32452 515.71446,175.889 517.58036,180.27232 C 515.81264,190.20371 515.39011,201.56061 517.08036,214.74107 C 521.20709,246.92099 537.59806,263.53147 553.01786,270.27232 C 568.43766,277.01318 582.83036,274.83482 582.83036,274.83482 C 582.83036,274.83483 577.71526,275.24823 582.83036,274.77232 C 587.50751,274.33717 591.86036,274.55815 602.29911,268.42857 C 602.29914,268.42858 606.39286,276.30357 606.39286,276.30357 C 606.39288,276.30356 624.48661,247.86607 624.48661,247.86607 C 624.48662,247.86606 590.01786,245.89732 590.01786,245.89732 C 590.01789,245.89735 594.14286,253.99107 594.14286,253.99107 C 586.35086,257.71478 585.44887,257.61978 580.48661,257.80357 C 580.4866,257.80358 570.57214,259.06976 559.95536,254.42857 C 549.33858,249.78739 537.76946,240.1025 534.23661,212.55357 C 533.72028,208.5273 533.48366,204.84141 533.33036,201.30357 C 540.81977,207.31075 550.1704,211.54446 561.51786,212.70982 C 582.35971,214.85024 599.4766,206.04859 606.61161,191.58482 C 613.74662,177.12106 610.27268,158.41655 596.36161,143.74107 C 584.29923,131.01587 562.27612,128.2905 543.64286,138.86607 C 538.46382,141.80551 533.81951,145.99509 529.76786,151.17857 C 530.29581,136.75864 535.2846,120.98749 544.04911,108.45982 C 548.73707,107.79117 553.51772,107.42857 558.39286,107.42857 z M 566.17411,150.20982 C 573.89893,150.02985 580.8779,152.53434 583.79911,155.61607 C 593.86962,166.23996 594.70705,176.60795 591.08036,183.95982 C 587.45367,191.3117 578.75788,197.07865 563.29911,195.49107 C 549.028,194.02547 540.31674,187.68753 535.26786,178.20982 C 538.51434,165.32757 545.13451,157.91048 552.20536,153.89732 C 556.64613,151.37691 561.53922,150.3178 566.17411,150.20982 z "
+       style="fill:black;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:17.29999924;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1" />
+    <rect
+       y="411.64795"
+       x="-10.000001"
+       height="260.71429"
+       width="809.28571"
+       id="rect3888"
+       style="opacity:1;fill:black;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.18600011;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <rect
+       style="opacity:1;fill:black;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.18600011;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect3872"
+       width="142.14285"
+       height="259.28574"
+       x="18.57143"
+       y="763.07648" />
+    <path
+       style="fill:#7a7a7a;fill-opacity:0.5139665;fill-rule:nonzero;stroke:none;stroke-width:17.29999924;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1"
+       d="M 158.39286,772.97321 C 93.491402,772.97321 40.642862,825.82176 40.642862,890.72321 C 40.642852,955.62466 93.491412,1008.4732 158.39286,1008.4732 C 223.27328,1008.4732 276.10866,955.65833 276.14286,890.78571 C 276.14286,890.7752 276.14286,890.76497 276.14286,890.75446 C 276.14287,890.74404 276.14288,890.73363 276.14286,890.72321 C 276.14895,890.53576 276.14896,890.34816 276.14286,890.16071 C 276.14234,890.05515 276.1124,889.95371 276.11161,889.84821 C 275.62946,825.29614 222.98559,772.97322 158.39286,772.97321 z M 158.39286,790.28571 C 213.76734,790.28572 258.56335,834.84936 258.83036,890.22321 C 258.82847,890.32737 258.82848,890.43155 258.83036,890.53571 C 258.82967,890.59821 258.82968,890.66071 258.83036,890.72321 C 258.83036,946.26427 213.93392,991.16071 158.39286,991.16071 C 102.85179,991.16072 57.955362,946.26428 57.955362,890.72321 C 57.955352,848.50733 83.901482,812.44273 120.73661,797.56696 C 113.30382,814.1958 110.56347,832.63036 113.42411,849.44196 C 114.23061,854.18166 115.71446,858.74614 117.58036,863.12946 C 115.81264,873.06085 115.39011,884.41775 117.08036,897.59821 C 121.20709,929.77813 137.59806,946.38861 153.01786,953.12946 C 168.43766,959.87032 182.83036,957.69196 182.83036,957.69196 C 182.83036,957.69197 177.71526,958.10537 182.83036,957.62946 C 187.50751,957.19431 191.86036,957.41529 202.29911,951.28571 C 202.29914,951.28572 206.39286,959.16071 206.39286,959.16071 C 206.39288,959.1607 224.48661,930.72321 224.48661,930.72321 C 224.48662,930.7232 190.01786,928.75446 190.01786,928.75446 C 190.01789,928.75449 194.14286,936.84821 194.14286,936.84821 C 186.35086,940.57192 185.44887,940.47692 180.48661,940.66071 C 180.4866,940.66072 170.57214,941.9269 159.95536,937.28571 C 149.33858,932.64453 137.76946,922.95964 134.23661,895.41071 C 133.72028,891.38444 133.48366,887.69855 133.33036,884.16071 C 140.81977,890.16789 150.1704,894.4016 161.51786,895.56696 C 182.35971,897.70738 199.4766,888.90573 206.61161,874.44196 C 213.74662,859.9782 210.27268,841.27369 196.36161,826.59821 C 184.29923,813.87301 162.27612,811.14764 143.64286,821.72321 C 138.46382,824.66265 133.81951,828.85223 129.76786,834.03571 C 130.29581,819.61578 135.2846,803.84463 144.04911,791.31696 C 148.73707,790.64831 153.51772,790.28571 158.39286,790.28571 z M 166.17411,833.06696 C 173.89893,832.88699 180.8779,835.39148 183.79911,838.47321 C 193.86962,849.0971 194.70705,859.46509 191.08036,866.81696 C 187.45367,874.16884 178.75788,879.93579 163.29911,878.34821 C 149.028,876.88261 140.31674,870.54467 135.26786,861.06696 C 138.51434,848.18471 145.13451,840.76762 152.20536,836.75446 C 156.64613,834.23405 161.53922,833.17494 166.17411,833.06696 z "
+       id="path3866"
+       inkscape:export-filename="/home/alex/foreign/bruce/examples/pyglet-watermark.png"
+       inkscape:export-xdpi="48.919998"
+       inkscape:export-ydpi="48.919998" />
+    <path
+       style="fill:white;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:17.29999924;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1"
+       d="M 572.67858,432.97321 C 507.77712,432.97321 454.92858,485.82176 454.92858,550.72321 C 454.92857,615.62466 507.77713,668.47322 572.67858,668.47321 C 637.559,668.47321 690.39438,615.65833 690.42858,550.78571 C 690.42858,550.7752 690.42858,550.76497 690.42858,550.75446 C 690.42859,550.74404 690.4286,550.73363 690.42858,550.72321 C 690.43467,550.53576 690.43468,550.34816 690.42858,550.16071 C 690.42806,550.05515 690.39812,549.95371 690.39733,549.84821 C 689.91518,485.29614 637.27131,432.97322 572.67858,432.97321 z M 572.67858,450.28571 C 628.05306,450.28572 672.84907,494.84936 673.11608,550.22321 C 673.11419,550.32737 673.1142,550.43155 673.11608,550.53571 C 673.11539,550.59821 673.1154,550.66071 673.11608,550.72321 C 673.11608,606.26427 628.21964,651.16071 572.67858,651.16071 C 517.13751,651.16072 472.24108,606.26428 472.24108,550.72321 C 472.24107,508.50733 498.1872,472.44273 535.02233,457.56696 C 527.58954,474.1958 524.84919,492.63036 527.70983,509.44196 C 528.51633,514.18166 530.00018,518.74614 531.86608,523.12946 C 530.09836,533.06085 529.67583,544.41775 531.36608,557.59821 C 535.49281,589.77813 551.88378,606.38861 567.30358,613.12946 C 582.72338,619.87032 597.11608,617.69196 597.11608,617.69196 C 597.11608,617.69197 592.00098,618.10537 597.11608,617.62946 C 601.79323,617.19431 606.14608,617.41529 616.58483,611.28571 C 616.58486,611.28572 620.67858,619.16071 620.67858,619.16071 C 620.6786,619.1607 638.77233,590.72321 638.77233,590.72321 C 638.77234,590.7232 604.30358,588.75446 604.30358,588.75446 C 604.30361,588.75449 608.42858,596.84821 608.42858,596.84821 C 600.63658,600.57192 599.73459,600.47692 594.77233,600.66071 C 594.77232,600.66072 584.85786,601.9269 574.24108,597.28571 C 563.6243,592.64453 552.05518,582.95964 548.52233,555.41071 C 548.006,551.38444 547.76938,547.69855 547.61608,544.16071 C 555.10549,550.16789 564.45612,554.4016 575.80358,555.56696 C 596.64543,557.70738 613.76232,548.90573 620.89733,534.44196 C 628.03234,519.9782 624.5584,501.27369 610.64733,486.59821 C 598.58495,473.87301 576.56184,471.14764 557.92858,481.72321 C 552.74954,484.66265 548.10523,488.85223 544.05358,494.03571 C 544.58153,479.61578 549.57032,463.84463 558.33483,451.31696 C 563.02279,450.64831 567.80344,450.28571 572.67858,450.28571 z M 580.45983,493.06696 C 588.18465,492.88699 595.16362,495.39148 598.08483,498.47321 C 608.15534,509.0971 608.99277,519.46509 605.36608,526.81696 C 601.73939,534.16884 593.0436,539.93579 577.58483,538.34821 C 563.31372,536.88261 554.60246,530.54467 549.55358,521.06696 C 552.80006,508.18471 559.42023,500.76762 566.49108,496.75446 C 570.93185,494.23405 575.82494,493.17494 580.45983,493.06696 z "
+       id="path3876" />
+    <g
+       id="g3882"
+       transform="translate(-1.428571,-290.7142)">
+      <path
+         transform="translate(-80.29672,241.3815)"
+         sodipodi:open="true"
+         sodipodi:end="6.2783633"
+         sodipodi:start="0"
+         d="M 348.50263,254.34167 A 109.09647,109.09647 0 1 1 348.50136,253.81561"
+         sodipodi:ry="109.09647"
+         sodipodi:rx="109.09647"
+         sodipodi:cy="254.34167"
+         sodipodi:cx="239.40616"
+         id="path3884"
+         style="fill:#ff6161;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:17.29999924;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:type="arc" />
+      <path
+         style="fill:black;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:17.29999924;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1"
+         d="M 159.10715,377.97321 C 94.205689,377.97321 41.357149,430.82176 41.357149,495.72321 C 41.357139,560.62466 94.205699,613.47322 159.10715,613.47321 C 223.98757,613.47321 276.82295,560.65833 276.85715,495.78571 C 276.85715,495.7752 276.85715,495.76497 276.85715,495.75446 C 276.85716,495.74404 276.85717,495.73363 276.85715,495.72321 C 276.86324,495.53576 276.86325,495.34816 276.85715,495.16071 C 276.85663,495.05515 276.82669,494.95371 276.8259,494.84821 C 276.34375,430.29614 223.69988,377.97322 159.10715,377.97321 z M 159.10715,395.28571 C 214.48163,395.28572 259.27764,439.84936 259.54465,495.22321 C 259.54276,495.32737 259.54277,495.43155 259.54465,495.53571 C 259.54396,495.59821 259.54397,495.66071 259.54465,495.72321 C 259.54465,551.26427 214.64821,596.16071 159.10715,596.16071 C 103.56608,596.16072 58.669649,551.26428 58.669649,495.72321 C 58.669639,453.50733 84.615769,417.44273 121.4509,402.56696 C 114.01811,419.1958 111.27776,437.63036 114.1384,454.44196 C 114.9449,459.18166 116.42875,463.74614 118.29465,468.12946 C 116.52693,478.06085 116.1044,489.41775 117.79465,502.59821 C 121.92138,534.77813 138.31235,551.38861 153.73215,558.12946 C 169.15195,564.87032 183.54465,562.69196 183.54465,562.69196 C 183.54465,562.69197 178.42955,563.10537 183.54465,562.62946 C 188.2218,562.19431 192.57465,562.41529 203.0134,556.28571 C 203.01343,556.28572 207.10715,564.16071 207.10715,564.16071 C 207.10717,564.1607 225.2009,535.72321 225.2009,535.72321 C 225.20091,535.7232 190.73215,533.75446 190.73215,533.75446 C 190.73218,533.75449 194.85715,541.84821 194.85715,541.84821 C 187.06515,545.57192 186.16316,545.47692 181.2009,545.66071 C 181.20089,545.66072 171.28643,546.9269 160.66965,542.28571 C 150.05287,537.64453 138.48375,527.95964 134.9509,500.41071 C 134.43457,496.38444 134.19795,492.69855 134.04465,489.16071 C 141.53406,495.16789 150.88469,499.4016 162.23215,500.56696 C 183.074,502.70738 200.19089,493.90573 207.3259,479.44196 C 214.46091,464.9782 210.98697,446.27369 197.0759,431.59821 C 185.01352,418.87301 162.99041,416.14764 144.35715,426.72321 C 139.17811,429.66265 134.5338,433.85223 130.48215,439.03571 C 131.0101,424.61578 135.99889,408.84463 144.7634,396.31696 C 149.45136,395.64831 154.23201,395.28571 159.10715,395.28571 z M 166.8884,438.06696 C 174.61322,437.88699 181.59219,440.39148 184.5134,443.47321 C 194.58391,454.0971 195.42134,464.46509 191.79465,471.81696 C 188.16796,479.16884 179.47217,484.93579 164.0134,483.34821 C 149.74229,481.88261 141.03103,475.54467 135.98215,466.06696 C 139.22863,453.18471 145.8488,445.76762 152.91965,441.75446 C 157.36042,439.23405 162.25351,438.17494 166.8884,438.06696 z "
+         id="path3886" />
+    </g>
+    <text
+       id="text3862"
+       y="43.790756"
+       x="14.285713"
+       style="font-size:16px;font-style:normal;font-weight:normal;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       xml:space="preserve"><tspan
+         style="font-size:32px"
+         y="43.790756"
+         x="14.285713"
+         id="tspan3864"
+         sodipodi:role="line">Logo, white background</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:16px;font-style:normal;font-weight:normal;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       x="4.999999"
+       y="402.36218"
+       id="text3160"><tspan
+         sodipodi:role="line"
+         id="tspan3162"
+         x="4.999999"
+         y="402.36218"
+         style="font-size:32px">Logo, for dark background</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:16px;font-style:normal;font-weight:normal;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       x="458.57147"
+       y="45.933605"
+       id="text3868"><tspan
+         sodipodi:role="line"
+         id="tspan3870"
+         x="458.57147"
+         y="45.933605"
+         style="font-size:32px">Logo, monochrome</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:16px;font-style:normal;font-weight:normal;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       x="437.85718"
+       y="403.07645"
+       id="text3878"><tspan
+         sodipodi:role="line"
+         id="tspan3880"
+         x="437.85718"
+         y="403.07645"
+         style="font-size:32px">Logo, white monochrome</tspan></text>
+    <text
+       id="text3152"
+       y="755.93359"
+       x="20.714323"
+       style="font-size:16px;font-style:normal;font-weight:normal;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       xml:space="preserve"><tspan
+         style="font-size:32px"
+         y="755.93359"
+         x="20.714323"
+         id="tspan3154"
+         sodipodi:role="line">Logo, grey watermark</tspan></text>
+    <g
+       id="g3858"
+       transform="translate(2.857143,37.85715)"
+       inkscape:export-filename="/home/alex/foreign/bruce/examples/pyglet-dark.png"
+       inkscape:export-xdpi="48.919998"
+       inkscape:export-ydpi="48.919998">
+      <path
+         transform="translate(-75.29672,252.8102)"
+         sodipodi:open="true"
+         sodipodi:end="6.2783633"
+         sodipodi:start="0"
+         d="M 348.50263,254.34167 A 109.09647,109.09647 0 1 1 348.50136,253.81561"
+         sodipodi:ry="109.09647"
+         sodipodi:rx="109.09647"
+         sodipodi:cy="254.34167"
+         sodipodi:cx="239.40616"
+         id="path3122"
+         style="fill:black;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:17.29999924;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:type="arc" />
+      <path
+         style="fill:#eb5353;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:17.29999924;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1"
+         d="M 164.10715,389.40187 C 99.205689,389.40187 46.357149,442.25042 46.357149,507.15187 C 46.357139,572.05332 99.205699,624.90188 164.10715,624.90187 C 228.98757,624.90187 281.82295,572.08699 281.85715,507.21437 C 281.85715,507.20386 281.85715,507.19363 281.85715,507.18312 C 281.85716,507.1727 281.85717,507.16229 281.85715,507.15187 C 281.86324,506.96442 281.86325,506.77682 281.85715,506.58937 C 281.85663,506.48381 281.82669,506.38237 281.8259,506.27687 C 281.34375,441.7248 228.69988,389.40188 164.10715,389.40187 z M 164.10715,406.71437 C 219.48163,406.71438 264.27764,451.27802 264.54465,506.65187 C 264.54276,506.75603 264.54277,506.86021 264.54465,506.96437 C 264.54396,507.02687 264.54397,507.08937 264.54465,507.15187 C 264.54465,562.69293 219.64821,607.58937 164.10715,607.58937 C 108.56608,607.58938 63.669649,562.69294 63.669649,507.15187 C 63.669639,464.93599 89.615769,428.87139 126.4509,413.99562 C 119.01811,430.62446 116.27776,449.05902 119.1384,465.87062 C 119.9449,470.61032 121.42875,475.1748 123.29465,479.55812 C 121.52693,489.48951 121.1044,500.84641 122.79465,514.02687 C 126.92138,546.20679 143.31235,562.81727 158.73215,569.55812 C 174.15195,576.29898 188.54465,574.12062 188.54465,574.12062 C 188.54465,574.12063 183.42955,574.53403 188.54465,574.05812 C 193.2218,573.62297 197.57465,573.84395 208.0134,567.71437 C 208.01343,567.71438 212.10715,575.58937 212.10715,575.58937 C 212.10717,575.58936 230.2009,547.15187 230.2009,547.15187 C 230.20091,547.15186 195.73215,545.18312 195.73215,545.18312 C 195.73218,545.18315 199.85715,553.27687 199.85715,553.27687 C 192.06515,557.00058 191.16316,556.90558 186.2009,557.08937 C 186.20089,557.08938 176.28643,558.35556 165.66965,553.71437 C 155.05287,549.07319 143.48375,539.3883 139.9509,511.83937 C 139.43457,507.8131 139.19795,504.12721 139.04465,500.58937 C 146.53406,506.59655 155.88469,510.83026 167.23215,511.99562 C 188.074,514.13604 205.19089,505.33439 212.3259,490.87062 C 219.46091,476.40686 215.98697,457.70235 202.0759,443.02687 C 190.01352,430.30167 167.99041,427.5763 149.35715,438.15187 C 144.17811,441.09131 139.5338,445.28089 135.48215,450.46437 C 136.0101,436.04444 140.99889,420.27329 149.7634,407.74562 C 154.45136,407.07697 159.23201,406.71437 164.10715,406.71437 z M 171.8884,449.49562 C 179.61322,449.31565 186.59219,451.82014 189.5134,454.90187 C 199.58391,465.52576 200.42134,475.89375 196.79465,483.24562 C 193.16796,490.5975 184.47217,496.36445 169.0134,494.77687 C 154.74229,493.31127 146.03103,486.97333 140.98215,477.49562 C 144.22863,464.61337 150.8488,457.19628 157.91965,453.18312 C 162.36042,450.66271 167.25351,449.6036 171.8884,449.49562 z "
+         id="path3832" />
+    </g>
+  </g>
+  <g
+     inkscape:groupmode="layer"
+     id="layer2"
+     style="display:none">
+    <rect
+       style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:red;stroke-width:0.30000028;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3093"
+       width="54.888397"
+       height="51.045933"
+       x="121.46758"
+       y="-271.82065"
+       transform="matrix(-0.299064,0.954233,-0.954233,-0.299064,0,0)" />
+    <rect
+       style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:red;stroke-width:0.30000016;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3095"
+       width="66.855545"
+       height="8.107563"
+       x="246.88493"
+       y="-72.342659"
+       transform="matrix(0.477823,0.878456,-0.878456,0.477823,0,0)" />
+    <rect
+       transform="matrix(0.893354,-0.449354,0.449354,0.893354,0,0)"
+       y="268.07974"
+       x="42.61515"
+       height="8.3796721"
+       width="74.02739"
+       id="rect3102"
+       style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:red;stroke-width:0.30000007;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    <rect
+       transform="matrix(0.893354,-0.449354,0.449354,0.893354,0,0)"
+       y="276.46503"
+       x="42.595081"
+       height="8.379674"
+       width="74.027405"
+       id="rect3104"
+       style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:red;stroke-width:0.30000013;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    <rect
+       transform="matrix(0.893354,-0.449354,0.449354,0.893354,0,0)"
+       y="259.63715"
+       x="42.578911"
+       height="8.3796759"
+       width="74.02742"
+       id="rect3106"
+       style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:red;stroke-width:0.30000019;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    <rect
+       style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:red;stroke-width:0.30000019;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3100"
+       width="74.02742"
+       height="8.3796759"
+       x="42.631546"
+       y="284.8226"
+       transform="matrix(0.893354,-0.449354,0.449354,0.893354,0,0)" />
+  </g>
+</svg>
diff --git a/doc/_static/relatedlogo.png b/doc/_static/relatedlogo.png
new file mode 100644
index 0000000..e823bb7
Binary files /dev/null and b/doc/_static/relatedlogo.png differ
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644
index 0000000..968fc42
--- /dev/null
+++ b/doc/conf.py
@@ -0,0 +1,334 @@
+# -*- coding: utf-8 -*-
+#
+# pyglet documentation build configuration file.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+
+
+import os
+import sys
+import time
+import datetime
+
+# Prevents instance attributes from having a default value of None
+# See sphinx ticket: https://github.com/sphinx-doc/sphinx/issues/2044
+from sphinx.ext.autodoc import (ClassLevelDocumenter, InstanceAttributeDocumenter)
+
+
+def iad_add_directive_header(self, sig):
+    ClassLevelDocumenter.add_directive_header(self, sig)
+
+
+InstanceAttributeDocumenter.add_directive_header = iad_add_directive_header
+
+
+def write_build(data, filename):
+    with open(os.path.join('internal', filename), 'w') as f:
+        f.write(".. list-table::\n")
+        f.write("   :widths: 50 50\n")
+        f.write("\n")
+        for var, val in data:
+            f.write("   * - "+var+"\n     - "+val+"\n")
+
+
+sys.is_pyglet_doc_run = True
+
+document_modules = ["pyglet"]
+
+# Patched extensions base path.
+sys.path.insert(0, os.path.abspath('.'))
+
+# import the pyglet package.
+sys.path.insert(0, os.path.abspath('..'))
+
+
+try:
+    import pyglet
+    print("Generating pyglet %s Documentation" % pyglet.version)
+except ImportError:
+    print("ERROR: pyglet not found")
+    sys.exit(1)
+
+
+# -- PYGLET DOCUMENTATION CONFIGURATION ----------------------------------------
+
+
+implementations = ["cocoa", "win32", "xlib"]
+
+# For each module, a list of submodules that should not be imported.
+# If value is None, do not try to import any submodule.
+skip_modules = {"pyglet": {
+                     "pyglet.com": None,
+                     "pyglet.compat": None,
+                     "pyglet.lib": None,
+                     "pyglet.libs": None,
+                     "pyglet.app": implementations,
+                     "pyglet.canvas": implementations + ["xlib_vidmoderestore"],
+                     "pyglet.extlibs": None,
+                     "pyglet.font": ["quartz",
+                                     "win32",
+                                     "freetype", "freetype_lib",
+                                     "fontconfig",
+                                     "win32query",],
+                     "pyglet.input": ["darwin_hid",
+                                      "directinput",
+                                      "evdev",
+                                      "wintab",
+                                      "x11_xinput", "x11_xinput_tablet"],
+                     "pyglet.image.codecs": ["gdiplus",
+                                             "gdkpixbuf2",
+                                             "pil",
+                                             "quartz",
+                                             "quicktime"],
+                     "pyglet.gl": implementations + ["agl",
+                                                     "glext_arb", "glext_nv",
+                                                     "glx", "glx_info",
+                                                     "glxext_arb", "glxext_mesa", "glxext_nv",
+                                                     "lib_agl", "lib_glx", "lib_wgl",
+                                                     "wgl", "wgl_info", "wglext_arb", "wglext_nv"],
+                     "pyglet.media.drivers": ["directsound",
+                                              "openal",
+                                              "pulse"],
+                     "pyglet.window": implementations,
+                        },
+                }
+
+
+now = datetime.datetime.fromtimestamp(time.time())
+build_data = (("Date", now.strftime("%Y/%m/%d %H:%M:%S")),
+              ("pyglet version", pyglet.version))
+write_build(build_data, 'build.rst')
+
+# -- SPHINX STANDARD OPTIONS ---------------------------------------------------
+
+autosummary_generate = False
+
+# -- General configuration -----------------------------------------------------
+#
+# Note that not all possible configuration values are present in this file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+inheritance_graph_attrs = dict(rankdir="LR", size='""')
+
+# 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',
+              'ext.docstrings',
+              'sphinx.ext.inheritance_diagram', 
+              'sphinx.ext.todo',
+              'sphinx.ext.napoleon']
+
+autodoc_member_order = 'groupwise'
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'pyglet'
+copyright = u'2006-2008, Alex Holkner. 2008-2020 pyglet contributors'
+
+# 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.5'
+# The full version, including alpha/beta/rc tags.
+release = pyglet.version
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+# language = 'en'
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build', '_templates', 'api']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+add_module_names = False
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+modindex_common_prefix = ['pyglet.']
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+html_theme = 'sphinx_rtd_theme'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+# html_theme_path = ["ext/theme"]
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+html_title = "pyglet v%s" % pyglet.version
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+html_short_title = "pyglet v%s documentation " % pyglet.version
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+html_logo = "_static/logo.png"
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+html_favicon = "_static/favicon.ico"
+
+# 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']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+html_domain_indices = True
+
+# If false, no index is generated.
+html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+html_split_index = True
+
+# If true, links to the reST sources are added to the pages.
+html_show_sourcelink = False
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'pygletdoc'
+
+
+# -- 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': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'pyglet.tex', u'pyglet Documentation',
+   u'Alex Holkner', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    ('index', 'pyglet', u'pyglet Documentation',
+     [u'Alex Holkner'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- 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 = [
+  ('index', 'pyglet', u'pyglet Documentation',
+   u'Alex Holkner', 'pyglet', 'One line description of project.',
+   'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
diff --git a/doc/ext/README.md b/doc/ext/README.md
new file mode 100644
index 0000000..4894158
--- /dev/null
+++ b/doc/ext/README.md
@@ -0,0 +1,9 @@
+
+# Custom Theme
+
+The `ext` directory contains the old sphinx theme that was replaced with
+`sphinx-rtd-theme` in 2019. The custom theme is somewhat broken with newer
+versions of sphinx.
+
+We probably want to spend our valuable time improving pyglet than maintaining a custom theme, but be free to
+revive this in the future!
diff --git a/doc/ext/__init__.py b/doc/ext/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/doc/ext/docstrings.py b/doc/ext/docstrings.py
new file mode 100644
index 0000000..98a63ff
--- /dev/null
+++ b/doc/ext/docstrings.py
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+''' pyglet specific docstring transformations.
+'''
+
+_debug = False
+
+def debug(lines):
+    with open('debug.log', 'a') as f:
+        for line in lines:
+            f.write(line+"\n")
+
+if _debug:
+    with open('debug.log', 'w') as f:
+        f.write("Docstring modifications.\n\n")
+
+
+def indentation(line):
+    if line.strip()=="": return 0
+    return len(line) - len(line.lstrip())
+
+
+def process_block(converter, lines, start):
+    ''' Apply a transformation to an indented block
+    '''
+    first = indentation(lines[start])
+    current = first
+    blocks = [[]]
+    block = 0
+
+    for i, line in enumerate(lines[start+1:]):
+        level = indentation(line)
+        if level<=first:
+            try: # allow one blank line in the block
+                if line.strip()=="" and \
+                   (current == indentation(lines[start + i + 2])):
+                    continue
+            except: pass
+            break
+        if level<current:
+            blocks.append([line])
+            block += 1
+        else:
+            blocks[block].append(line)
+        current = level
+
+    result = []
+    for block in blocks:
+            result += converter(block)
+
+    return i, result
+
+
+def ReST_parameter(lines):
+    ''' Converts :parameters: blocks to :param: markup
+    '''
+    indent = indentation(lines[0])
+    part = lines[0].replace("`","").split(":")
+    name = part[0].replace(" ", "")
+    rest = [":param "+name+":"]
+    for line in lines[1:]:
+        rest.append(line[indent:])
+    if len(part)>1:
+        rest.append(":type "+name+": "+part[1].strip().lstrip())
+    return rest
+
+
+def ReST_Ivariable(lines):
+    ''' Converts :Ivariable: blocks to :var: markup
+    '''
+    indent = indentation(lines[0])
+    part = lines[0].replace("`","").split(":")
+    name = part[0].replace(" ", "")
+    rest = [":var "+name+":"]
+    for line in lines[1:]:
+        rest.append(line[indent:])
+    if len(part)>1:
+        rest.append(":vartype "+name+": "+part[1].strip().lstrip())
+    return rest
+
+
+
+
+def modify_docstrings(app, what, name, obj, options, lines,
+                      reference_offset=[0]):
+
+    original = lines[:]
+
+    def convert(converter, start):
+        affected, result = process_block(converter, lines, start)
+        for x in range(start, start+affected+1):
+            del lines[start]
+        for i, line in enumerate(result):
+            lines.insert(start+i, line)
+
+    i=0
+    while i<len(lines):
+        line = lines[i]
+        if ":parameters:" in line.lower():
+            convert(ReST_parameter, i)
+
+        elif ":Ivariables:" in line:
+            convert(ReST_Ivariable, i)
+
+        elif ":deprecated:" in line:
+            lines[i] = lines[i].replace(u':deprecated:',
+                                u'.. warning:: Deprecated.')
+            lines.insert(i,"")
+            lines.insert(i,"")
+
+        elif ":event:" in line.lower():
+            lines[i] = lines[i].replace(u':event:', u'.. event mark')
+
+        i += 1
+
+
+    if _debug and original!=lines:
+        title = what + " " +name
+        debug(["\n",title, "-"*len(title)])
+        debug(["Original:", ""]+original)
+        debug(["Redacted:", ""]+lines)
+
+
+def setup(app):
+    app.connect('autodoc-process-docstring', modify_docstrings)
diff --git a/doc/ext/theme/pyglet/layout.html b/doc/ext/theme/pyglet/layout.html
new file mode 100644
index 0000000..08b5738
--- /dev/null
+++ b/doc/ext/theme/pyglet/layout.html
@@ -0,0 +1,204 @@
+{#
+    basic/layout.html
+    ~~~~~~~~~~~~~~~~~
+
+    Master layout template for Sphinx themes.
+
+    :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+#}
+{%- block doctype -%}
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+{%- endblock %}
+{%- set reldelim1 = reldelim1 is not defined and ' &raquo;' or reldelim1 %}
+{%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %}
+{%- set render_sidebar = (not embedded) and (not theme_nosidebar|tobool) and
+                         (sidebars != []) %}
+{%- set url_root = pathto('', 1) %}
+{# XXX necessary? #}
+{%- if url_root == '#' %}{% set url_root = '' %}{% endif %}
+{%- if not embedded and docstitle %}
+  {%- set titlesuffix = " &mdash; "|safe + docstitle|e %}
+{%- else %}
+  {%- set titlesuffix = "" %}
+{%- endif %}
+
+{%- macro relbar() %}
+    <div class="related">
+      <h3>{{ _('Navigation') }}</h3>
+      <ul>
+        {%- for rellink in rellinks %}
+        <li class="right" {% if loop.first %}style="margin-right: 10px"{% endif %}>
+          <a href="{{ pathto(rellink[0]) }}" title="{{ rellink[1]|striptags|e }}"
+             {{ accesskey(rellink[2]) }}>{{ rellink[3] }}</a>
+          {%- if not loop.first %}{{ reldelim2 }}{% endif %}</li>
+        {%- endfor %}
+        {%- block rootrellink %}
+		<li><a href="http://pyglet.org/">pyglet.org</a>{{ reldelim2 }}</li>
+		<li><a href="{{ pathto(master_doc) }}">Documentation Index</a>{{ reldelim1 }}</li>
+        {%- endblock %}
+        {%- for parent in parents %}
+          <li><a href="{{ parent.link|e }}" {% if loop.last %}{{ accesskey("U") }}{% endif %}>{{ parent.title }}</a>{{ reldelim1 }}</li>
+        {%- endfor %}
+        {%- block relbaritems %} {% endblock %}
+      </ul>
+    </div>
+{%- endmacro %}
+
+{%- macro sidebar() %}
+      {%- if render_sidebar %}
+      <div class="sphinxsidebar">
+        <div class="sphinxsidebarwrapper">
+          {%- block sidebarlogo %}
+          {%- if logo %}
+            <p class="logo"><a href="{{ pathto(master_doc) }}">
+              <img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/>
+            </a></p>
+          {%- endif %}
+          {%- endblock %}
+          {%- if sidebars != None %}
+            {#- new style sidebar: explicitly include/exclude templates #}
+            {%- for sidebartemplate in sidebars %}
+            {%- include sidebartemplate %}
+            {%- endfor %}
+          {%- else %}
+            {#- old style sidebars: using blocks -- should be deprecated #}
+            {%- block sidebartoc %}
+            {%- include "localtoc.html" %}
+            {%- endblock %}
+            {%- block sidebarrel %}
+            {%- include "relations.html" %}
+            {%- endblock %}
+            {%- block sidebarsourcelink %}
+            {%- include "sourcelink.html" %}
+            {%- endblock %}
+            {%- if customsidebar %}
+            {%- include customsidebar %}
+            {%- endif %}
+            {%- block sidebarsearch %}
+            {%- include "searchbox.html" %}
+            {%- endblock %}
+          {%- endif %}
+        </div>
+      </div>
+      {%- endif %}
+{%- endmacro %}
+
+{%- macro script() %}
+    <script type="text/javascript">
+      var DOCUMENTATION_OPTIONS = {
+        URL_ROOT:    '{{ url_root }}',
+        VERSION:     '{{ release|e }}',
+        COLLAPSE_INDEX: false,
+        FILE_SUFFIX: '{{ '' if no_search_suffix else file_suffix }}',
+        HAS_SOURCE:  {{ has_source|lower }}
+      };
+    </script>
+    {%- for scriptfile in script_files %}
+    <script type="text/javascript" src="{{ pathto(scriptfile, 1) }}"></script>
+    {%- endfor %}
+{%- endmacro %}
+
+{%- macro css() %}
+    <link rel="stylesheet" href="{{ pathto('_static/' + style, 1) }}" type="text/css" />
+    <link rel="stylesheet" href="{{ pathto('_static/pygments.css', 1) }}" type="text/css" />
+    {%- for cssfile in css_files %}
+    <link rel="stylesheet" href="{{ pathto(cssfile, 1) }}" type="text/css" />
+    {%- endfor %}
+{%- endmacro %}
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset={{ encoding }}" />
+    {{ metatags }}
+    {%- block htmltitle %}
+    <title>{{ title|striptags|e }}{{ titlesuffix }}</title>
+    {%- endblock %}
+    {{ css() }}
+    {%- if not embedded %}
+    {{ script() }}
+    {%- if use_opensearch %}
+    <link rel="search" type="application/opensearchdescription+xml"
+          title="{% trans docstitle=docstitle|e %}Search within {{ docstitle }}{% endtrans %}"
+          href="{{ pathto('_static/opensearch.xml', 1) }}"/>
+    {%- endif %}
+    {%- if favicon %}
+    <link rel="shortcut icon" href="{{ pathto('_static/' + favicon, 1) }}"/>
+    {%- endif %}
+    {%- endif %}
+{%- block linktags %}
+    {%- if hasdoc('about') %}
+    <link rel="author" title="{{ _('About these documents') }}" href="{{ pathto('about') }}" />
+    {%- endif %}
+    {%- if hasdoc('genindex') %}
+    <link rel="index" title="{{ _('Index') }}" href="{{ pathto('genindex') }}" />
+    {%- endif %}
+    {%- if hasdoc('search') %}
+    <link rel="search" title="{{ _('Search') }}" href="{{ pathto('search') }}" />
+    {%- endif %}
+    {%- if hasdoc('copyright') %}
+    <link rel="copyright" title="{{ _('Copyright') }}" href="{{ pathto('copyright') }}" />
+    {%- endif %}
+    <link rel="top" title="{{ docstitle|e }}" href="{{ pathto('index') }}" />
+    {%- if parents %}
+    <link rel="up" title="{{ parents[-1].title|striptags|e }}" href="{{ parents[-1].link|e }}" />
+    {%- endif %}
+    {%- if next %}
+    <link rel="next" title="{{ next.title|striptags|e }}" href="{{ next.link|e }}" />
+    {%- endif %}
+    {%- if prev %}
+    <link rel="prev" title="{{ prev.title|striptags|e }}" href="{{ prev.link|e }}" />
+    {%- endif %}
+{%- endblock %}
+{%- block extrahead %} {% endblock %}
+  </head>
+  <body>
+{%- block header %}{% endblock %}
+
+{%- block relbar1 %}{{ relbar() }}{% endblock %}
+
+{%- block content %}
+  {%- block sidebar1 %} {# possible location for sidebar #} {% endblock %}
+
+    <div class="document">
+  {%- block document %}
+      <div class="documentwrapper">
+      {%- if render_sidebar %}
+        <div class="bodywrapper">
+      {%- endif %}
+          <div class="body">
+            {% block body %} {% endblock %}
+          </div>
+      {%- if render_sidebar %}
+        </div>
+      {%- endif %}
+      </div>
+  {%- endblock %}
+
+  {%- block sidebar2 %}{{ sidebar() }}{% endblock %}
+      <div class="clearer"></div>
+    </div>
+{%- endblock %}
+
+{%- block relbar2 %}{{ relbar() }}{% endblock %}
+
+{%- block footer %}
+    <div class="footer">
+    {%- if show_copyright %}
+      {%- if hasdoc('copyright') %}
+        {% trans path=pathto('copyright'), copyright=copyright|e %}&copy; <a href="{{ path }}">Copyright</a> {{ copyright }}.{% endtrans %}
+      {%- else %}
+        {% trans copyright=copyright|e %}&copy; Copyright {{ copyright }}.{% endtrans %}
+      {%- endif %}
+    {%- endif %}
+    {%- if last_updated %}
+      {% trans last_updated=last_updated|e %}Last updated on {{ last_updated }}.{% endtrans %}
+    {%- endif %}
+    {%- if show_sphinx %}
+      {% trans sphinx_version=sphinx_version|e %}Created using <a href="http://sphinx.pocoo.org/">Sphinx</a> {{ sphinx_version }}.{% endtrans %}
+    {%- endif %}
+    </div>
+{%- endblock %}
+  </body>
+</html>
diff --git a/doc/ext/theme/pyglet/static/basic.css_t b/doc/ext/theme/pyglet/static/basic.css_t
new file mode 100644
index 0000000..2937fa4
--- /dev/null
+++ b/doc/ext/theme/pyglet/static/basic.css_t
@@ -0,0 +1,540 @@
+/*
+ * basic.css
+ * ~~~~~~~~~
+ *
+ * Sphinx stylesheet -- basic theme.
+ *
+ * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ */
+
+/* -- main layout ----------------------------------------------------------- */
+
+div.clearer {
+    clear: both;
+}
+
+/* -- relbar ---------------------------------------------------------------- */
+
+div.related {
+    width: 100%;
+    font-size: 90%;
+}
+
+div.related h3 {
+    display: none;
+}
+
+div.related ul {
+    margin: 0;
+    padding: 0 0 0 10px;
+    list-style: none;
+}
+
+div.related li {
+    display: inline;
+}
+
+div.related li.right {
+    float: right;
+    margin-right: 5px;
+}
+
+/* -- sidebar --------------------------------------------------------------- */
+
+div.sphinxsidebarwrapper {
+    padding: 10px 5px 0 10px;
+}
+
+div.sphinxsidebar {
+    float: left;
+    width: {{ theme_sidebarwidth|toint }}px;
+    margin-left: -100%;
+    font-size: 90%;
+}
+
+div.sphinxsidebar ul {
+    list-style: none;
+}
+
+div.sphinxsidebar ul ul,
+div.sphinxsidebar ul.want-points {
+    margin-left: 20px;
+    list-style: square;
+}
+
+div.sphinxsidebar ul ul {
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+div.sphinxsidebar form {
+    margin-top: 10px;
+}
+
+div.sphinxsidebar input {
+    border: 1px solid #98dbcc;
+    font-family: sans-serif;
+    font-size: 1em;
+}
+
+div.sphinxsidebar #searchbox input[type="text"] {
+    width: 170px;
+}
+
+div.sphinxsidebar #searchbox input[type="submit"] {
+    width: 30px;
+}
+
+img {
+    border: 0;
+}
+
+/* -- search page ----------------------------------------------------------- */
+
+ul.search {
+    margin: 10px 0 0 20px;
+    padding: 0;
+}
+
+ul.search li {
+    padding: 5px 0 5px 20px;
+    background-image: url(file.png);
+    background-repeat: no-repeat;
+    background-position: 0 7px;
+}
+
+ul.search li a {
+    font-weight: bold;
+}
+
+ul.search li div.context {
+    color: #888;
+    margin: 2px 0 0 30px;
+    text-align: left;
+}
+
+ul.keywordmatches li.goodmatch a {
+    font-weight: bold;
+}
+
+/* -- index page ------------------------------------------------------------ */
+
+table.contentstable {
+    width: 90%;
+}
+
+table.contentstable p.biglink {
+    line-height: 150%;
+}
+
+a.biglink {
+    font-size: 1.3em;
+}
+
+span.linkdescr {
+    font-style: italic;
+    padding-top: 5px;
+    font-size: 90%;
+}
+
+/* -- general index --------------------------------------------------------- */
+
+table.indextable {
+    width: 100%;
+}
+
+table.indextable td {
+    text-align: left;
+    vertical-align: top;
+}
+
+table.indextable dl, table.indextable dd {
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+table.indextable tr.pcap {
+    height: 10px;
+}
+
+table.indextable tr.cap {
+    margin-top: 10px;
+    background-color: #f2f2f2;
+}
+
+img.toggler {
+    margin-right: 3px;
+    margin-top: 3px;
+    cursor: pointer;
+}
+
+div.modindex-jumpbox {
+    border-top: 1px solid #ddd;
+    border-bottom: 1px solid #ddd;
+    margin: 1em 0 1em 0;
+    padding: 0.4em;
+}
+
+div.genindex-jumpbox {
+    border-top: 1px solid #ddd;
+    border-bottom: 1px solid #ddd;
+    margin: 1em 0 1em 0;
+    padding: 0.4em;
+}
+
+/* -- general body styles --------------------------------------------------- */
+
+a.headerlink {
+    visibility: hidden;
+}
+
+h1:hover > a.headerlink,
+h2:hover > a.headerlink,
+h3:hover > a.headerlink,
+h4:hover > a.headerlink,
+h5:hover > a.headerlink,
+h6:hover > a.headerlink,
+dt:hover > a.headerlink {
+    visibility: visible;
+}
+
+div.body p.caption {
+    text-align: inherit;
+}
+
+div.body td {
+    text-align: left;
+}
+
+.field-list ul {
+    padding-left: 1em;
+}
+
+.first {
+    margin-top: 0 !important;
+}
+
+p.rubric {
+    margin-top: 30px;
+    font-weight: bold;
+}
+
+img.align-left, .figure.align-left, object.align-left {
+    clear: left;
+    float: left;
+    margin-right: 1em;
+}
+
+img.align-right, .figure.align-right, object.align-right {
+    clear: right;
+    float: right;
+    margin-left: 1em;
+}
+
+img.align-center, .figure.align-center, object.align-center {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.align-left {
+    text-align: left;
+}
+
+.align-center {
+    text-align: center;
+}
+
+.align-right {
+    text-align: right;
+}
+
+/* -- sidebars -------------------------------------------------------------- */
+
+div.sidebar {
+    margin: 0 0 0.5em 1em;
+    border: 1px solid #ddb;
+    padding: 7px 7px 0 7px;
+    background-color: #ffe;
+    width: 40%;
+    float: right;
+}
+
+p.sidebar-title {
+    font-weight: bold;
+}
+
+/* -- topics ---------------------------------------------------------------- */
+
+div.topic {
+    border: 1px solid #ccc;
+    padding: 7px 7px 0 7px;
+    margin: 10px 0 10px 0;
+}
+
+p.topic-title {
+    font-size: 1.1em;
+    font-weight: bold;
+    margin-top: 10px;
+}
+
+/* -- admonitions ----------------------------------------------------------- */
+
+div.admonition {
+    margin-top: 10px;
+    margin-bottom: 10px;
+    padding: 7px;
+}
+
+div.admonition dt {
+    font-weight: bold;
+}
+
+div.admonition dl {
+    margin-bottom: 0;
+}
+
+p.admonition-title {
+    margin: 0px 10px 5px 0px;
+    font-weight: bold;
+}
+
+div.body p.centered {
+    text-align: center;
+    margin-top: 25px;
+}
+
+/* -- tables ---------------------------------------------------------------- */
+
+table.docutils {
+    border: 0;
+    border-collapse: collapse;
+}
+
+table.docutils td, table.docutils th {
+    padding: 1px 8px 1px 5px;
+    border-top: 0;
+    border-left: 0;
+    border-right: 0;
+    border-bottom: 1px solid #aaa;
+}
+
+table.field-list td, table.field-list th {
+    border: 0 !important;
+}
+
+table.footnote td, table.footnote th {
+    border: 0 !important;
+}
+
+th {
+    text-align: left;
+    padding-right: 5px;
+}
+
+table.citation {
+    border-left: solid 1px gray;
+    margin-left: 1px;
+}
+
+table.citation td {
+    border-bottom: none;
+}
+
+/* -- other body styles ----------------------------------------------------- */
+
+ol.arabic {
+    list-style: decimal;
+}
+
+ol.loweralpha {
+    list-style: lower-alpha;
+}
+
+ol.upperalpha {
+    list-style: upper-alpha;
+}
+
+ol.lowerroman {
+    list-style: lower-roman;
+}
+
+ol.upperroman {
+    list-style: upper-roman;
+}
+
+dl {
+    margin-bottom: 15px;
+}
+
+dd p {
+    margin-top: 0px;
+}
+
+dd ul, dd table {
+    margin-bottom: 10px;
+}
+
+dd {
+    margin-top: 3px;
+    margin-bottom: 10px;
+    margin-left: 30px;
+}
+
+dt:target, .highlighted {
+    background-color: #fbe54e;
+}
+
+dl.glossary dt {
+    font-weight: bold;
+    font-size: 1.1em;
+}
+
+.field-list ul {
+    margin: 0;
+    padding-left: 1em;
+}
+
+.field-list p {
+    margin: 0;
+}
+
+.refcount {
+    color: #060;
+}
+
+.optional {
+    font-size: 1.3em;
+}
+
+.versionmodified {
+    font-style: italic;
+}
+
+.system-message {
+    background-color: #fda;
+    padding: 5px;
+    border: 3px solid red;
+}
+
+.footnote:target  {
+    background-color: #ffa;
+}
+
+.line-block {
+    display: block;
+    margin-top: 1em;
+    margin-bottom: 1em;
+}
+
+.line-block .line-block {
+    margin-top: 0;
+    margin-bottom: 0;
+    margin-left: 1.5em;
+}
+
+.guilabel, .menuselection {
+    font-family: sans-serif;
+}
+
+.accelerator {
+    text-decoration: underline;
+}
+
+.classifier {
+    font-style: oblique;
+}
+
+abbr, acronym {
+    border-bottom: dotted 1px;
+    cursor: help;
+}
+
+/* -- code displays --------------------------------------------------------- */
+
+pre {
+    overflow: auto;
+    overflow-y: hidden;  /* fixes display issues on Chrome browsers */
+}
+
+td.linenos pre {
+    padding: 5px 0px;
+    border: 0;
+    background-color: transparent;
+    color: #aaa;
+}
+
+table.highlighttable {
+    margin-left: 0.5em;
+}
+
+table.highlighttable td {
+    padding: 0 0.5em 0 0.5em;
+}
+
+tt.descname {
+    background-color: transparent;
+    font-weight: bold;
+    font-size: 1.2em;
+}
+
+tt.descclassname {
+    background-color: transparent;
+}
+
+tt.xref, a tt {
+    background-color: transparent;
+    font-weight: bold;
+}
+
+h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt {
+    background-color: transparent;
+}
+
+.viewcode-link {
+    float: right;
+}
+
+.viewcode-back {
+    float: right;
+    font-family: sans-serif;
+}
+
+div.viewcode-block:target {
+    margin: -1px -10px;
+    padding: 0 10px;
+}
+
+/* -- math display ---------------------------------------------------------- */
+
+img.math {
+    vertical-align: middle;
+}
+
+div.body div.math p {
+    text-align: center;
+}
+
+span.eqno {
+    float: right;
+}
+
+/* -- printout stylesheet --------------------------------------------------- */
+
+@media print {
+    div.document,
+    div.documentwrapper,
+    div.bodywrapper {
+        margin: 0 !important;
+        width: 100%;
+    }
+
+    div.sphinxsidebar,
+    div.related,
+    div.footer,
+    #top-link {
+        display: none;
+    }
+}
diff --git a/doc/ext/theme/pyglet/static/pyglet.css_t b/doc/ext/theme/pyglet/static/pyglet.css_t
new file mode 100644
index 0000000..f89bd77
--- /dev/null
+++ b/doc/ext/theme/pyglet/static/pyglet.css_t
@@ -0,0 +1,304 @@
+/*
+ * pyglet.css_t
+ * ~~~~~~~~~~~~
+ *
+ * Sphinx stylesheet 
+ *
+ * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ */
+ 
+@import url("basic.css");
+ 
+/* -- page layout ----------------------------------------------------------- */
+ 
+body {
+    font-family: Arial, sans-serif;
+    font-size: 100%;
+    background-color: #111;
+    color: #555;
+    margin: 0;
+    padding: 0;
+}
+
+div.documentwrapper {
+    float: left;
+    width: 100%;
+}
+
+div.bodywrapper {
+    margin: 0 0 0 230px;
+}
+
+hr {
+    border: 1px solid #B1B4B6;
+}
+ 
+div.document {
+    background-color: #eee;
+}
+ 
+div.body {
+    background-color: #ffffff;
+    color: #3E4349;
+    padding: 0 30px 30px 30px;
+    font-size: 0.9em;
+}
+ 
+div.footer {
+    color: #555;
+    width: 100%;
+    padding: 13px 0;
+    text-align: center;
+    font-size: 75%;
+}
+ 
+div.footer a {
+    color: #444;
+    text-decoration: underline;
+}
+ 
+div.related {
+    background-color: #FF6161;
+    line-height: 32px;
+    color: #000000;
+    font-size: 0.9em;
+}
+ 
+div.related a {
+    color: #000000;
+    font-size: 110%;
+    font-weight: bold;
+}
+ 
+div.sphinxsidebar {
+    font-size: 0.75em;
+    line-height: 1.5em;
+}
+
+div.sphinxsidebarwrapper{
+    padding: 0px 0;
+}
+ 
+div.sphinxsidebar h3,
+div.sphinxsidebar h4 {
+    font-family: Arial, sans-serif;
+    color: #222;
+    font-size: 1.2em;
+    font-weight: normal;
+    margin: 0;
+    padding: 5px 10px;
+    background-color: #ddd;
+}
+
+div.sphinxsidebar h4{
+    font-size: 1.1em;
+}
+ 
+div.sphinxsidebar h3 a {
+    color: #222;
+    font-size: 105%;
+}
+ 
+ 
+div.sphinxsidebar p {
+    color: #888;
+    padding: 5px 20px;
+}
+ 
+div.sphinxsidebar p.topless {
+}
+ 
+div.sphinxsidebar ul {
+    margin: 10px 20px;
+    padding: 0;
+    color: #000;
+}
+ 
+div.sphinxsidebar a {
+    color: #000;
+}
+ 
+div.sphinxsidebar input {
+    border: 1px solid #ccc;
+    font-family: sans-serif;
+    font-size: 1em;
+}
+
+div.sphinxsidebar input[type=text]{
+    margin-left: 20px;
+}
+ 
+/* -- body styles ----------------------------------------------------------- */
+ 
+a {
+    color: #005B81;
+    text-decoration: none;
+}
+ 
+a:hover {
+    color: #A44;
+    text-decoration: underline;
+}
+ 
+div.body h1,
+div.body h2,
+div.body h3,
+div.body h4,
+div.body h5,
+div.body h6 {
+    font-family: Arial, sans-serif;
+    font-weight: normal;
+    color: #220000;
+    margin: 30px 0px 10px 0px;
+    padding: 5px 0 5px 10px;
+    }
+ 
+div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; padding: 5px 0 5px 0px;}
+div.body h2 { font-size: 150%; background-color: #FFEEEE; }
+div.body h3 { font-size: 120%; background-color: #D8DEE3; }
+div.body h4 { font-size: 110%; background-color: #D8DEE3; }
+div.body h5 { font-size: 100%; background-color: #D8DEE3; }
+div.body h6 { font-size: 100%; background-color: #D8DEE3; }
+ 
+a.headerlink {
+    color: #c60f0f;
+    font-size: 0.8em;
+    padding: 0 4px 0 4px;
+    text-decoration: none;
+}
+ 
+a.headerlink:hover {
+    background-color: #c60f0f;
+    color: white;
+}
+ 
+div.body p, div.body dd, div.body li {
+    line-height: 1.5em;
+}
+ 
+div.admonition p.admonition-title + p {
+    display: inline;
+}
+
+div.highlight pre{
+    background-color: #FFFDDD;
+}
+
+div.note {
+    background-color: #FCFCFC;
+    border: 1px solid #ccc;
+}
+ 
+div.seealso {
+    background-color: #ffc;
+    border: 1px solid #ff6;
+}
+ 
+div.topic {
+    background-color: #eee;
+}
+ 
+div.warning {
+    background-color: #ffe4e4;
+    border: 1px solid #f66;
+}
+ 
+p.admonition-title {
+    display: inline;
+}
+ 
+p.admonition-title:after {
+    content: ":";
+}
+ 
+pre {
+    padding: 10px;
+    background-color: White;
+    color: #222;
+    line-height: 1.2em;
+    border: 1px solid #C6C9CB;
+    font-size: 1.1em;
+    margin: 1.5em 0 1.5em 0;
+}
+ 
+tt {
+    background-color: #FFFDDD;
+    font-size: 16px;
+    font-family: monospace;
+}
+
+h1 tt {
+    font-size: 1.1em;
+    font-weight: bold;
+}
+
+.viewcode-back {
+    font-family: Arial, sans-serif;
+}
+
+div.viewcode-block:target {
+    background-color: #f4debf;
+    border-top: 1px solid #ac9;
+    border-bottom: 1px solid #ac9;
+}
+
+
+div.related tt, div.related a {
+    color: #000;
+    font-family:Arial,sans-serif;
+    font-size: 16px;
+}
+
+th.field-name {
+    font-size: 14px;
+    padding-left: 0px !important;
+}
+
+
+tt.descname {
+    font-size: 17px;
+    font-weight: bold;
+    color: #000;
+}
+
+
+tt.xref, p tt {
+    font-size: 15px;
+    position: relative;
+    bottom: 1px;
+    font-weight: normal;
+    } 
+
+p.rubric {
+    font-weight: normal;
+    font-size: 22px;
+}
+
+img.logo {
+    width: 192px;
+}
+
+.section dl {
+    padding: 5px 0px 10px 0px;
+}
+
+.section dt {
+    margin: 0px 0px 10px 0px;
+
+}
+
+pre, tt, code {
+    font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+}
+
+/* Specific API doc styles */
+
+dt {
+    font-size: 1.2rem;
+}
+
+dl.class dl > dt {
+    font-size: 1.0rem;
+}
\ No newline at end of file
diff --git a/doc/ext/theme/pyglet/theme.conf b/doc/ext/theme/pyglet/theme.conf
new file mode 100644
index 0000000..b73e2b6
--- /dev/null
+++ b/doc/ext/theme/pyglet/theme.conf
@@ -0,0 +1,8 @@
+[theme]
+inherit = basic
+stylesheet = pyglet.css
+pygments_style = tango
+
+[options]
+nosidebar = false
+sidebarwidth = 230
\ No newline at end of file
diff --git a/doc/external_resources.rst b/doc/external_resources.rst
new file mode 100644
index 0000000..69c90da
--- /dev/null
+++ b/doc/external_resources.rst
@@ -0,0 +1,79 @@
+
+
+Related Documentation
+=====================
+
+* `OpenGL Programming Guide <http://www.glprogramming.com/red/>`_
+* `OpenGL Reference Pages <http://opengl.org/sdk/docs/man/>`_
+* `ctypes Reference <http://docs.python.org/3/library/ctypes.html>`_
+* `Python Documentation <http://docs.python.org/>`_
+
+Third party libraries
+=====================
+
+Listed here are a few third party libraries that you might find useful when
+developing your project.  Please direct any questions to the respective
+authors. If you would like to have your library listed here, let us know!
+
+glooey
+------
+
+An object-oriented GUI library for pyglet
+(https://glooey.readthedocs.io).
+
+Every game needs a user interface that matches its look and feel. The
+purpose of glooey is to help you make such an interface.  Towards this
+end, glooey provides 7 powerful placement widgets, a label widget, an
+image widget, 3 different button widgets, a text entry widget, avariety
+of scroll boxes and bars, 4 different dialog box widgets, and a variety
+of other miscellaneous widgets.  The appearance of any widget can be
+trivially customized, and glooey comes with built-in fantasy, puzzle,
+and 8-bit themes to prove it (and to help you hit the ground running if
+your game fits one of those genres).
+
+PyShaders
+---------
+
+Pythonic OpenGL shader wrapper for python
+(https://github.com/gabdube/pyshaders)
+
+Pyshaders aims to completely wraps the opengl2.1 shader api in a python
+module. Pyshaders provides a pythonic OOP api that hides the lower level
+(ctypes) calls. Pyshaders provides a high level api and a low level api,
+and it can be integrated easily with existing code because it does not
+occlude the underlying opengl values.
+
+Ratcave
+-------
+
+A Simple Python 3D Graphics Engine extension for pyglet, Psychopy
+and PyGame (https://github.com/neuroneuro15/ratcave).
+
+Ratcave provides a simple OOP interface for loading, positioning, and
+drawing 3D scenes in OpenGL windows.  It's a great fit for simple games
+and scientific behavioral experiments!
+
+Projects using pyglet
+=====================
+
+pyglet is a fairly lightweight library, which makes it ideal to build upon.
+Listed here are a few projects that take advantage of pyglet "under the hood".
+If you would like to have your project listed here, let us know!
+
+cocos2d
+-------
+
+A framework for building 2D games, demos, and other graphical/interactive
+applications (http://python.cocos2d.org).
+
+Cocos2d is an open source software framework. It can be used to build
+games, apps and other cross platform GUI based interactive programs.
+
+Arcade
+------
+
+A 2D library for game development focusing on simplicity.
+(https://arcade.academy)
+
+Arcade builds on Pyglet with a focus to make creating 2D arcade games
+simple and easy for hobbyists and new programmers.
diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644
index 0000000..d2ec122
--- /dev/null
+++ b/doc/index.rst
@@ -0,0 +1,101 @@
+pyglet Documentation
+====================
+
+**pyglet** is a cross-platform windowing and multimedia library for Python,
+intended for developing games and other visually rich applications. It supports
+windowing, user interface event handling, game controllers and joysticks,
+OpenGL graphics, loading images and videos, and playing sounds and music.
+**pyglet** works on Windows, OS X and Linux.
+
+Some of the features of pyglet are:
+
+* **No external dependencies or installation requirements.** For most
+  application and game requirements, pyglet needs nothing else besides Python,
+  simplifying distribution and installation.
+* **Take advantage of multiple windows and multi-monitor desktops.** pyglet
+  allows you to use as many windows as you need, and is fully aware of
+  multi-monitor setups for use with fullscreen games and applications.
+* **Load images, sound, music and video in almost any format.** pyglet has
+  built-in support for common audio and image formats, and can optionally use
+  ffmpeg to load almost any other compressed audio or video files.
+* **pyglet is provided under the BSD open-source license**, allowing you to
+  use it for both commercial and other open-source projects with very little
+  restriction.
+
+Please join our `Discord` server, or join us on the `mailing list`_!
+
+.. _Discord: https://discord.gg/QXyegWe
+.. _mailing list: http://groups.google.com/group/pyglet-users
+
+If this is your first time reading about pyglet, we suggest you start at
+:doc:`programming_guide/quickstart`.
+
+.. toctree::
+   :maxdepth: 3
+   :caption: Programming Guide
+
+   programming_guide/installation
+   programming_guide/quickstart
+   programming_guide/windowing
+   programming_guide/keyboard
+   programming_guide/mouse
+   programming_guide/input
+   programming_guide/graphics
+   programming_guide/shapes
+   programming_guide/text
+   programming_guide/image
+   programming_guide/media
+   programming_guide/resources
+   programming_guide/events
+   programming_guide/time
+   programming_guide/context
+   programming_guide/gl
+   programming_guide/eventloop
+   programming_guide/options
+   programming_guide/debug
+   programming_guide/advanced
+   programming_guide/examplegame
+
+.. toctree::
+   :maxdepth: 3
+   :caption: API Reference
+
+   modules/pyglet
+   modules/app
+   modules/canvas
+   modules/clock
+   modules/event
+   modules/font
+   modules/gl
+   modules/graphics/index
+   modules/gui
+   modules/image/index
+   modules/info
+   modules/input
+   modules/media
+   modules/resource
+   modules/sprite
+   modules/shapes
+   modules/text/index
+   modules/window
+
+.. toctree::
+   :maxdepth: 3
+   :caption: External Resources
+
+   external_resources
+
+.. toctree::
+   :maxdepth: 3
+   :caption: Development Guide
+
+   internal/contributing
+   internal/virtualenv
+   internal/testing
+   internal/doc
+   internal/dist
+   internal/gl
+   internal/generated
+   internal/wraptypes
+   internal/media_manual
+   internal/media_logging_manual
diff --git a/doc/internal/blacklist.rst b/doc/internal/blacklist.rst
new file mode 100644
index 0000000..0e39e06
--- /dev/null
+++ b/doc/internal/blacklist.rst
@@ -0,0 +1,54 @@
+* ``pyglet.app.cocoa``
+* ``pyglet.app.win32``
+* ``pyglet.app.xlib``
+* ``pyglet.canvas.cocoa``
+* ``pyglet.canvas.win32``
+* ``pyglet.canvas.xlib``
+* ``pyglet.canvas.xlib_vidmoderestore``
+* ``pyglet.com``
+* ``pyglet.compat``
+* ``pyglet.extlibs``
+* ``pyglet.font.fontconfig``
+* ``pyglet.font.freetype``
+* ``pyglet.font.freetype_lib``
+* ``pyglet.font.quartz``
+* ``pyglet.font.win32``
+* ``pyglet.font.win32query``
+* ``pyglet.gl.agl``
+* ``pyglet.gl.cocoa``
+* ``pyglet.gl.glext_arb``
+* ``pyglet.gl.glext_nv``
+* ``pyglet.gl.glx``
+* ``pyglet.gl.glx_info``
+* ``pyglet.gl.glxext_arb``
+* ``pyglet.gl.glxext_mesa``
+* ``pyglet.gl.glxext_nv``
+* ``pyglet.gl.lib_agl``
+* ``pyglet.gl.lib_glx``
+* ``pyglet.gl.lib_wgl``
+* ``pyglet.gl.wgl``
+* ``pyglet.gl.wgl_info``
+* ``pyglet.gl.wglext_arb``
+* ``pyglet.gl.wglext_nv``
+* ``pyglet.gl.win32``
+* ``pyglet.gl.xlib``
+* ``pyglet.image.codecs.gdiplus``
+* ``pyglet.image.codecs.gdkpixbuf2``
+* ``pyglet.image.codecs.pil``
+* ``pyglet.image.codecs.quartz``
+* ``pyglet.image.codecs.quicktime``
+* ``pyglet.input.darwin_hid``
+* ``pyglet.input.directinput``
+* ``pyglet.input.evdev``
+* ``pyglet.input.wintab``
+* ``pyglet.input.x11_xinput``
+* ``pyglet.input.x11_xinput_tablet``
+* ``pyglet.lib``
+* ``pyglet.libs``
+* ``pyglet.media.drivers.directsound``
+* ``pyglet.media.drivers.openal``
+* ``pyglet.media.drivers.pulse``
+* ``pyglet.media.sources.ffmpeg``
+* ``pyglet.window.cocoa``
+* ``pyglet.window.win32``
+* ``pyglet.window.xlib``
diff --git a/doc/internal/contributing.rst b/doc/internal/contributing.rst
new file mode 100644
index 0000000..746a6dc
--- /dev/null
+++ b/doc/internal/contributing.rst
@@ -0,0 +1,79 @@
+Contributing
+============
+
+Roadmap
+-------
+
+Planned work for future versions can be found in the
+`roadmap <https://github.com/pyglet/pyglet/wiki/Roadmap>`_.
+
+Communication
+-------------
+
+Pyglet communication occurs mostly in our
+`mailing list <http://groups.google.com/group/pyglet-users>`_.
+
+Issue Tracker
+-------------
+
+You can use the `issue tracker <https://github.com/pyglet/pyglet/issues>`_
+to report any bug or compatibility issue.
+
+We prefer the tracker to address discussions on specific bugs, and address
+broader topics of pyglet in the mailing list.
+
+Getting the latest development version
+--------------------------------------
+
+The repository can be found `here <https://github.com/pyglet/pyglet>`_;
+it hosts the source, documentation, examples, and development tools. You can
+get the latest version of the code using:
+
+.. code-block:: bash
+
+    # clone over https
+    git clone https://github.com/pyglet/pyglet.git
+
+    # or clone over ssh
+    git clone git@github.com:pyglet/pyglet.git
+
+Contributing to the source
+--------------------------
+
+If you want to contribute to pyglet, we suggest the following:
+
+* Fork the `official repository <https://github.com/pyglet/pyglet/fork>`_
+* Apply your changes to your fork
+* Submit a `pull request <https://github.com/pyglet/pyglet/pulls>`_
+  describing the changes you have made
+* Alternatively, you can create a patch and submit it to the issue tracker.
+
+Contributing to the documentation
+---------------------------------
+
+When asking to include your code in the repository, check that you have
+addressed its respective documentation, both within the code and the API
+documentation. It is very important to all of us that the documentation matches
+the latest code and vice-versa.
+
+Consequently, an error in the documentation, either because it is hard to
+understand or because it doesn't match the code, is a bug that deserves to
+be reported on a ticket.
+
+A good way to start contributing to a component of pyglet is by its
+documentation. When studying the code you are going to work with, also read
+the associated docs. If you don't understand the code with the help of the
+docs, it is a sign that the docs should be improved.
+
+Contact
+-------
+
+pyglet is developed by many individual volunteers, and there is no central
+point of contact. If you have a question about developing with pyglet, or you
+wish to contribute, please join the
+`discord server <https://discord.gg/QXyegWe>`_,
+or the
+`mailing list <http://groups.google.com/group/pyglet-users>`_.
+
+For legal issues, please contact
+`Alex Holkner <mailto:Alex.Holkner@gmail.com>`_.
diff --git a/doc/internal/dist.rst b/doc/internal/dist.rst
new file mode 100755
index 0000000..ca3bdd4
--- /dev/null
+++ b/doc/internal/dist.rst
@@ -0,0 +1,47 @@
+Making a pyglet release
+=======================
+
+#. Clone pyglet into a new directory
+
+#. Make sure it is up to date::
+
+    git pull
+
+#. Update version string in the following files and commit:
+
+   * pyglet/__init__.py
+   * doc/conf.py
+
+#. Tag the current changelist with the version number::
+
+    git tag -a v0.0.0 -m "release message"
+
+#. Push the changes to the central repo::
+
+    git push
+    git push --tags
+
+#. Build the wheels and documentation::
+
+    ./make.py clean
+    ./make.py dist
+
+#. Upload the wheels and zips to PyPI::
+
+    twine upload dist/pyglet-x.y.z*
+
+#. Start a build of the documentation on https://readthedocs.org/projects/pyglet/builds/
+
+#. Draft a new release on Github, using the same version number https://github.com/pyglet/pyglet/releases
+
+#. Tell people!
+
+Major version increase
+----------------------
+When preparing for a major version you might also want to consider the
+following:
+
+* Create a maintenance branch for the major version
+* Add a readthedocs configuration for that maintenance branch
+* Point the url in setup.py to the maintenance branch documentation
+
diff --git a/doc/internal/doc.rst b/doc/internal/doc.rst
new file mode 100644
index 0000000..73fd884
--- /dev/null
+++ b/doc/internal/doc.rst
@@ -0,0 +1,146 @@
+Documentation
+=============
+
+This is the pyglet documentation, generated with `Sphinx`_.
+
+.. _Sphinx: https://sphinx-doc.org
+
+.. _reStructuredText: http://www.sphinx-doc.org/en/stable/rest.html
+
+.. _autodoc: http://www.sphinx-doc.org/en/stable/ext/autodoc.html
+
+Details:
+
+.. include:: build.rst
+
+.. note::
+
+   See the `Sphinx warnings log file <../warnings.txt>`_ for errors.
+
+
+Writing documentation
+---------------------
+
+Familiarize yourself with `Sphinx`_ and `reStructuredText`_.
+
+Literature
+^^^^^^^^^^
+
+The home page is ``pyglet/doc/index.rst``. This file create three toctrees:
+
+* The programming guide
+* The API docs
+* The development guide, which you are reading now
+
+Source code
+-----------
+
+The API documentation is generated from the source code docstrings via
+`autodoc`_ and a few custom extensions.
+
+:Example:
+
+   .. code-block:: python
+
+      class Class1():
+      '''Short description.
+
+      Detailed explanation, formatted as reST.
+      Can be as detailed as it is needed.
+
+      :Ivariables:
+         `arg1`
+             description
+
+      .. versionadded:: 1.2
+
+      '''
+
+      attribute1 = None
+      '''This is an attribute.
+
+      More details.
+      '''
+
+      #: This is another attribute.
+      attribute2 = None
+
+      def __init__(self):
+          '''Constructor
+
+          :parameters:
+             `arg1` : type
+                description
+          '''
+
+          self.instance_attribute = None
+          '''This is an instance attribute.
+          '''
+
+      def method(self):
+          '''Short description.
+
+          :returns: return description
+          :rtype: returned type
+          '''
+
+      def _get_property1(self):
+          '''Getter Method contains docstrings for a property
+
+          :return: property1 value
+          :rtype: property1 type
+          '''
+
+      def _set_property1(self, value):
+          '''Setter Method docstrings are ignored
+          '''
+
+      property1 = property(_get_property1, _set_property1,
+                        doc='''Override docstring here if you want''')
+
+
+Pyglet has a special role for deprecations, ``:deprecated:``.
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Source
+          - Output
+        * - ``:deprecated: Do not use``
+          - .. warning:: Deprecated. Do not use
+
+
+Building
+--------
+
+The complete documentation can be generated using ``sphinx``.
+Make sure you prepare your environment as stated in :doc:`virtualenv`.
+
+To build the documentation, execute::
+
+   ./make.py docs --open
+
+.. note ::
+   Due to a bug in Sphinx, documentation generation currently only works using Python 3.x.
+
+If the build succeeds, the web pages are in ``doc/_build/html``.
+
+Optionally the standalone way to build docs is through
+``setup.py`` or ``make``.
+
+.. code:: bash
+
+    # using setup.py (output dir: _build in project root)
+    python setup.py build_sphinx
+
+    # make (make.bat for windows)
+    cd doc
+    make html
+
+HTML Theme
+----------
+
+.. Note:: The custom theme was disabled in 2019 and replaced with
+          the standard Read the Docs theme ``sphinx_rtd_theme``.
+
+The custom sphinx theme is in the ``ext/theme`` folder.
diff --git a/doc/internal/generated.rst b/doc/internal/generated.rst
new file mode 100644
index 0000000..7435d23
--- /dev/null
+++ b/doc/internal/generated.rst
@@ -0,0 +1,117 @@
+ctypes Wrapper Generation
+=========================
+
+The following modules in pyglet are entirely (or mostly) generated from one or
+more C header files:
+
+* pyglet.gl.agl
+* pyglet.gl.gl
+* pyglet.gl.glext_abi
+* pyglet.gl.glext_nv
+* pyglet.gl.glu
+* pyglet.gl.glx
+* pyglet.gl.glxext_abi
+* pyglet.gl.glxext_nv
+* pyglet.gl.wgl
+* pyglet.gl.wglext_abi
+* pyglet.gl.wglext_nv
+* pyglet.window.xlib.xlib
+* pyglet.window.xlib.xinerama
+
+The wrapping framework is in ``tools/wraptypes``, and pyglet-specialised batch
+scripts are ``tools/genwrappers.py`` (generates xlib wrappers) and
+``tools/gengl.py`` (generates gl wrappers).
+
+Generating GL wrappers
+----------------------
+
+This process needs to be followed when the wraptypes is updated, the header
+files are updated (e.g., a new release of the operating system), or the GL
+extensions are updated.  Each file can only be generated a a specific
+platform.
+
+Before beginning, remove the file ``tools/.gengl.cache`` if it exists.  This
+merely caches header files so they don't need to be repeatedly downloaded (but
+you'd prefer to use the most recent uncached copies if you're reading this,
+presumably).
+
+On Linux, generate ``pyglet.gl.gl``, ``pyglet.gl.glext_abi``,
+``pyglet.gl.glext_nv`` and ``pyglet.gl.glu`` (the complete user-visible GL
+package)::
+
+    python tools/gengl.py gl glext_abi glext_nv glu
+
+The header files for ``pyglet.gl.gl`` and ``pyglet.gl.glu`` are located in
+``/usr/include/GL``.  Ensure your Linux distribution has recent versions
+of these files (unfortunately they do not seem to be accessible outside of a
+distribution or OS).
+
+The header files for ``pyglet.glext_abi`` and ``pyglet.glext_nv`` are
+downloaded from http://www.opengl.org and http://developer.nvidia.com,
+respectively.
+
+On Linux still, generate ``pyglet.gl.glx``, ``pyglet.gl.glxext_abi`` and
+``pyglet.gl.glxext_nv``::
+
+    python tools/gengl.py glx glxext_abi glxext_nv
+
+The header file for ``pyglet.gl.glx`` is in ``/usr/include/GL``, and
+is expected to depend on X11 header files from ``/usr/include/X11``.
+``glext_abi`` and ``glext_nv`` header files are downloaded from the above
+websites.
+
+On OS X, generate ``pyglet.gl.agl``::
+
+    python tools/gengl.py agl
+
+Watch a movie while you wait -- it uses virtually every header file on the
+system.  Expect to see one syntax error in ``PictUtils.h`` line 67, it is
+unimportant.
+
+On Windows XP, generate ``pyglet.gl.wgl``, ``pyglet.gl.wglext_abi`` and
+``pyglet.gl.wglext_nv``::
+
+    python tools/gengl.py wgl wglext_abi wglext_nv
+
+You do not need to have a development environment installed on Windows.
+``pyglet.gl.wgl`` is generated from ``tools/wgl.h``, which is a hand-coded
+header file containing the prototypes and constants for WGL and its
+dependencies.  In a real development environment you would find these mostly
+in ``WinGDI.h``, but wraptypes is not quite sophisticated enough to parse
+Windows system headers (see below for what needs implementing).  It is
+extremely unlikely this header will ever need to change (excepting a bug fix).
+
+The headers for ``pyglet.gl.wglext_abi`` and ``pyglet.gl.wglext_nv`` are
+downloaded from the same websites as for GL and GLX.
+
+Generated GL wrappers
+---------------------
+
+Each generated file contains a pair of markers ``# BEGIN GENERATED CONTENT``
+and ``# END GENERATED CONTENT`` which are searched for when replacing the
+file.  If either marker is missing or corrupt, the file will not be modified.
+This allows for custom content around the generated content.  Only ``glx.py``
+makes use of this, to include some additional enumerators that are not
+generated by default.
+
+If a generating process is interrupted (either you get sick of it, or it
+crashes), it will leave a partially-complete file written, which will not
+include both markers.  It is up to you to restore the file or otherwise
+reinsert the markers.
+
+Generating Xlib wrappers
+------------------------
+
+On Linux with the Xinerama extension installed (doesn't have to be in use,
+just available), run::
+
+    python tools/genwrappers.py
+
+This generates ``pyglet.window.xlib.xlib`` and
+``pyglet.window.xlib.xinerama``.
+
+Note that this process, as well as the generated modules, depend on
+``pyglet.gl.glx``.  So, you should always run this `after` the above GL
+generation.
+
+
diff --git a/doc/internal/gl.rst b/doc/internal/gl.rst
new file mode 100644
index 0000000..bf8147c
--- /dev/null
+++ b/doc/internal/gl.rst
@@ -0,0 +1,59 @@
+OpenGL Interface Implementation
+--------------------------------
+
+See `OpenGL Interface` for details on the publically-visible modules.
+
+See `ctypes Wrapper Generation` for details on some of these modules are
+generated.
+
+ctypes linkage
+==============
+
+Most functions link to libGL.so (Linux), opengl32.dll (Windows) or
+OpenGL.framework (OS X).  ``pyglet.gl.lib`` provides some helper types then
+imports linker functions for the appropriate platform: one of
+``pyglet.gl.lib_agl``, ``pyglet.gl.lib_glx``, ``pyglet.gl.lib_wgl``.
+
+On any platform, the following steps are taken to link each function during
+import:
+
+1. Look in the appropriate library (e.g. libGL.so, libGLU.so, opengl32.dll,
+   etc.) using ``cdll`` or ``windll``.
+
+2. If not found, call ``wglGetProcAddress`` or ``glxGetProcAddress`` to try to
+   resolve the function's address dynamically.  On OS X, skip this step.
+
+3. On Windows, this will fail if the context hasn't been created yet.  Create
+   and return a proxy object ``WGLFunctionProxy`` which will try the same
+   resolution again when the object is ``__call__``'d.
+
+   The proxy object caches its result so that subsequent calls have only a
+   single extra function-call overhead.
+
+4. If the function is still not found (either during import or proxy call),
+   the function is replaced with ``MissingFunction`` (defined in
+   ``pyglet.gl.lib``), which raises an exception.  The exception message
+   details the name of the function, and optionally the name of the extension
+   it requires and any alternative functions that can be used.
+
+   The extension required is currently guessed by ``gengl.py`` based on nearby
+   ``#ifndef`` declarations, it is occasionally wrong.
+
+   The suggestion list is not currently used, but is intended to be
+   implemented such that calling, for example, ``glCreateShader`` on an
+   older driver suggests ``glCreateShaderObjectARB``, etc.
+
+To access the linking function, import ``pyglet.gl.lib`` and use one of
+``link_AGL``, ``link_GLX``, ``link_WGL``, ``link_GL`` or ``link_GLU``.  This
+is what the generated modules do.
+
+Missing extensions
+==================
+
+The latest ``glext.h`` on opengl.org and nvidia does not include some recent
+extensions listed on the registry.  These must be hand coded into
+``pyglet.gl.glext_missing``.  They should be removed when ``glext.h`` is
+updated.
+
+
+
diff --git a/doc/internal/media_logging_manual.rst b/doc/internal/media_logging_manual.rst
new file mode 100644
index 0000000..d889657
--- /dev/null
+++ b/doc/internal/media_logging_manual.rst
@@ -0,0 +1,679 @@
+Media logging manual
+^^^^^^^^^^^^^^^^^^^^
+
+Workflows
+=========
+
+User submitting debug info
+--------------------------
+
+Basically:
+
+    - get samples
+    - run a script
+    - submit that directory
+
+This is detailed in ``tools/ffmpeg/readme_run_tests.txt``.
+
+Changing code in pyglet ffmpeg subsystem
+----------------------------------------
+
+Preparation like in readme_run_tests.txt, optionally install the library bokeh
+(http://bokeh.pydata.org/en/latest/index.html) for visualization support.
+
+The basic flow goes as:
+
+- initialize the active session subsystem:
+  set environment variable ``pyglet_mp_samples_dir`` to the desired
+  samples_dir.
+- record a session with the initial state::
+
+    configure.py new <session> [playlist]
+    run_test_suite.py
+
+- Follow this workflow
+
+  .. code-block:: none
+
+      while True:
+          edit code
+          commit to hg
+          record a new session:
+              configure.py new <new session> [playlist]
+              run_test_suite.py
+          look at the last session reports in samples_dir/session/reports
+          especially 00_summary.txt, which shows defects stats and list condensed
+              info about any sample failing;
+          then to look more details look at the individual reports.
+          compare with prev sessions if desired:
+              compare.py <session1> <session2>
+
+          render additional reports:
+              report.py sample
+
+          or visualize the data collected with:
+              bokeh_timeline.py sample
+
+          if results are as wanted, break
+      done, you may want to delete sessions for intermediate commits
+
+It is possible to return to a previous session to request additional reports::
+
+    configure.py activate <session>
+    report.py ...
+
+You can list the known sessions for the current samples_dir with::
+
+    configure.py list
+
+.. important::
+    All this debugging machinery depends on a detailed and accurate capture of
+    media_player related state, currently in examples/media_player.py and
+    pyglet.media.player.
+
+    Modifications in those modules may require matching modifications in
+    pyglet/media/sources/instrumentation.py, and further propagation to other
+    modules.
+
+
+Changing the debug code for pyglet ffmpeg
+-----------------------------------------
+
+For initial debugging of debug code, where there are misspellings and trivial
+errors to weed out, creating a new session for each run_test_suite.py run may
+be inconvenient.
+
+The flag ``dev_debug`` can be set to true in the session configuration file;
+this will allow to rewrite the session.
+
+Keep in mind that some raw data will be stale or misleading:
+
+    - The ones captured at session creation time (currently pyglet.info and
+      pyglet_changeset)
+    - The collected crashes info (new crashes will not be seen)
+    - If media_player.py crashes before doing any writing, the state recording
+      will be the previous recording.
+
+The reports using that stale raw data will obviously report stale data.
+
+So it is a good idea to switch to a normal workflow as soon as posible
+(simply creating a new session and deleting the special session).
+
+
+Session
+=======
+
+If ``playlist_file`` is not specified, then all files in samples_dir, except
+for the files with extension ".dbg", ".htm", ".html", ".json", ".log", ".pkl",
+".py", ".txt" will make the implicit playlist; subdirectories of samples_dir
+will **not** be explored.
+
+If a ``playlist_file`` is specified, then it should contain one sample name
+per line; a sanity check will be performed ensuring no blacklisted extension
+is used, and that the sample exists in samples_dir.
+
+Once the ``playlist_file`` is used in ``configure.py new`` a copy is writen to
+the session raw data directory, and this copy will be the authoritative
+playlist for the session; ``playlist_file`` can be deleted if desired.
+
+Specifying a playlist is useful in development to restrict the tests to
+samples relevant to the feature or issue under work.
+
+The session name will be used to create a samples_dir subdir to store the test
+results, hence it should be different of previous sessions names, and it must
+not contain slashes, ``/``, backslashes ``\`` or characters forbidden in
+directory names.
+
+
+Active session
+==============
+
+Most commands and subcommands target the currently active session.
+
+A session becomes active when
+
+    - a ``configure.py new session [playlist]`` is issued
+    - a ``configure.py activate session`` is issued
+
+The current implementation relies in two pieces of data to determine the
+active session
+
+    - the environment variable ``pyglet_mp_samples_dir`` specifies samples_dir,
+      the directory where all the media samples reside. Under the current
+      paths schema is also where session data will be stored, one subdir per
+      session.
+
+    - a file ``activation.json`` in samples_dir storing the name for the
+      current active session.
+
+Notice that the second precludes running two commands in parallel targeting
+two different sessions in the same sample_dir.
+
+The concept of active session plus the enforced path schema avoids the need to
+provide paths at each command invocation, making for less errors, easier docs
+and less typing.
+
+
+Commands Summary
+================
+
+Primary commands
+----------------
+
+They are the ones normally used by developers
+
+``configure.py``, ``mp.py`` : session creation, activation, protection, status
+and list all.
+
+``run_test_suite.py`` : plays session's samples, reports results.
+
+``report.py`` : produces the specified report for the specified sample.
+
+``timeline.py`` : translates the event stream to a stream of ``media_player``
+state, useful to pass to other software.
+
+``bokeh_timeline.py`` : visualization of data collected for the specified
+sample.
+
+Helper commands
+---------------
+
+Somehow an artifact of ``run_test_suite.py`` development, can help in testing
+the debugging subsystem. ``run_test_suite.py`` is basically ``playmany.py +
+retry_crashed.py + summarize.py``. When trying to change ``run_test_suite.py``
+it is easier to first adapt the relevant helper.
+
+``playmany.py`` : plays active session samples, recording media_player state
+along the play.
+
+``retry_crashed.py`` : plays again samples that have been seen always
+crashing, hoping to get a recording with no crash. Motivated by early tests on
+Ubuntu, where sometimes (but not always) a sample will crash the media_player.
+
+``summarize.py`` : using the raw data produced by the two previous commands
+elaborates some reports, aiming to give an idea of how well the run was and
+what samples should be investigated.
+
+Data directory layout
+=====================
+
+.. code-block:: none
+
+    samples_dir/ : directory where the samples live, also used to store
+                   sessions data
+        <session name>/ : directory to store session info, one per session,
+                          named as the session.
+            dbg/ : recording of media_player events captured while playing a
+                   sample, one per sample, named as sample.dbg; additional
+                   versioning info, other raw data collected.
+                _crashes_light.pkl : pickle with info for retry crashed
+                _pyglet_hg_revision.txt
+                _pyglet_info.txt
+                _samples_version.txt
+                _session_playlist.txt
+                <one .dbg file per sample in the session playlist, named sample.dbg>
+            reports/ : human readable reports rendered from the raw data (.txt),
+                       visualizations (.html), intermediate data used by other
+                       tools(.pkl)
+            configuration.json : session configuration info, mostly permissions
+        activation.json : holds the name of current active session
+        <sample> : one for each sample
+
+A subdirectory of samples_dir is detected as a session dir if:
+
+    - it is a direct child of session dir
+    - it has a ``configuration.json`` file
+
+policies:
+
+    - it should be hard to rewrite the .dbg files (recordings of media_player
+      states)
+    - think of dev analyzing data sent by an user.
+
+
+Code Layout and conventions
+===========================
+
+The emerging separation of responsabilities goes like
+
+Scripts (commands)
+------------------
+
+Structured as:
+
+    - uses ``if __main__`` idiom to allow use as module (testing, sharing)
+    - ``sysargs_to_mainargs()``: ``sys.argv`` translation to ``main`` params
+    - ``main(...)``
+
+        - params validation and translation to adequate code entities (uses
+          module ``fs``).
+        - translates exceptions to prints (uses module ``mpexceptions``)
+        - short chain of instantiations / function calls to accomplish the
+          command goals, no logic or calculations here.
+    - other functions and classes: code specific to this command, delegates as
+      much as possible to modules.
+
+When two scripts use some related but not identical functionality, these parts
+can be moved to another module. Example: at first ``summarize`` had the code to
+collect defects stats, later, when ``compare`` was writen, the module
+``extractors`` was added and the defect collection stats code moved to that
+module.
+
+If script B needs a subset of unchanged script A functionality, it imports A
+and uses what it needs. Example is ``retry_crashed``, will call into
+``playmany``.
+
+Because of the last point, some scripts will also be listed as modules.
+
+
+Modules
+-------
+
+
+buffered_logger
+_______________
+
+Accumulation of debug events while playing media_player, saves when
+sample's play ends
+
+
+instrumentation
+_______________
+
+Defines the events that modify media_player state.
+Defines which events are potential defects.
+Gives the low level support to extract info from the recorded data.
+
+For new code here, keep accepting and returning only data structures, no paths
+or files.
+
+
+fs
+__
+
+Path building for entities into a session directory should be delegated to
+``fs.PathServices``.
+Session's creation, activation and management at start of ``fs``.
+Versions capture are handled at start of module ``fs``.
+Utility functions to load - save at the end of ``fs``.
+
+While there isn't a ``Session`` object, in practice the code identifies and
+provides access to a particular session data by handling a ``fs.PathServices``
+instance.
+
+
+extractors
+__________
+
+Analyzes a media_player recording to build specific info on behalf of
+reporters. Uses ``instrumentation`` to get input data about the media_player
+state sequence seen while playing a sample.
+Defines object types to collect some specific info about a replay.
+
+
+reports
+_______
+
+Formats as text info captured / generated elsewhere.
+
+
+mpexceptions
+____________
+
+Defines exceptions generated by code in the ffmpeg debug subsystem.
+
+
+Scripts that also acts as modules
+---------------------------------
+
+timeline
+________
+
+Renders the media player's debug info to a format more suitable to postprocess
+in a spreadsheets or other software, particularly to get a data visualization.
+(used by ``bokeh_timeline.py``)
+
+playmany
+________
+
+Produces media_player debug recordings.
+Runs python scripts as subprocesses with a timeout (used by retry_crashed.py).
+
+
+Commands detailed
+=================
+
+bokeh_timeline.py
+-----------------
+
+Usage::
+
+    bokeh_timeline.py sample
+
+Renders media player's internal state graphically using bokeh.
+
+Arguments:
+
+.. code-block:: none
+
+    sample: sample to report
+
+The output will be written to session's output dir under
+``reports/sample.timeline.html``.
+
+Notice the plot can be zoomed live with the mouse wheel, but you must click
+the button that looks as a distorted **OP**; it also does pan with mouse drag.
+
+Example::
+
+    bokeh_timeline.py small.mp4
+
+will write the output to ``report/small.mp4.timeline.html``.
+
+
+compare.py
+----------
+
+Usage::
+
+    compare.py --reldir=relpath other_session
+
+Builds a reports comparing the active session with other_session.
+
+Outputs to ``samples_dir/relpath/comparison_<session>_<other_session>.txt``.
+
+
+configure.py
+------------
+
+Usage::
+
+    configure.py subcommand [args]
+
+Subcommands:
+
+.. code-block:: none
+
+    new session [playlist] : Creates a new session, sets it as the active one
+    activate session : activates a session
+    deactivate : no session will be active
+    protect [target]: forbids overwrite of session data
+    status : prints configuration for the active session
+    help [subcommand] : prints help for the given subcommand or topic
+    list : list all sessions associated the current samples_dir
+
+Creates and manages pyglet media_player debug session configurations.
+
+Most commands and subcommands need an environment variable
+``pyglet_mp_samples_dir`` to be set to the directory where the media samples
+reside.
+
+The configuration stores some values used when other commands are executed,
+mostly protection status.
+
+This command can be called both as ``configure.py`` or ``mp.py``, they do the
+same.
+
+
+mp.py
+-----
+
+alias for ``configure.py``
+
+
+playmany.py
+-----------
+
+Usage::
+
+    playmany.py
+
+Uses media_player to play a sequence of samples and record debug info.
+
+A session must be active, see command ``configure.py``
+If the active configuration has disallowed dbg overwrites it will do nothing.
+
+If a playlist was provided at session creation, then only the samples in the
+playlist will be played, otherwise all files in ``samples_dir``.
+
+
+report.py
+---------
+
+Usage::
+
+    report.py sample report_name
+
+Generates a report from the debugging info recorded while playing sample.
+
+Arguments:
+
+.. code-block:: none
+
+    sample: sample to report
+    report_name: desired report, one of
+        "anomalies": Start, end and interesting events
+        "all": All data is exposed as text
+        "counter": How many occurrences of each defect
+
+The report will be written to session's output dir under
+``reports/sample.report_name.txt``.
+
+Example::
+
+    report anomalies small.mp4
+
+will write the report *anomalies* to ``report/small.mp4.anomalies.txt``.
+
+The authoritative list of reports available comes from
+``reports.available_reports``
+
+
+retry_crashed.py
+----------------
+
+Usage::
+
+    retry_crashed.py [--clean] [max_retries]
+
+Inspects the raw data collected to get the list of samples that crashed the
+last time they were played.
+Then it replays those samples, recording new raw data for them.
+
+The process is repeated until all samples has a recording with no crashes or
+the still crashing samples were played ``max_tries`` times in this command
+run.
+
+Notice that only samples recorded as crashing in the last run are retried.
+
+A configuration must be active, see command ``configure.py``.
+
+Besides the updated debug recordings, a state is build and saved:
+
+.. code-block:: none
+
+    total_retries: total retries attempted, including previous runs
+    sometimes_crashed: list of samples that crashed one time but later
+                       completed a play
+    always_crashed: list of samples that always crashed
+
+Options:
+
+.. code-block:: none
+
+    --clean: discards crash data collected in a previous run
+    max_retries: defaults to 5
+
+
+run_test_suite.py
+-----------------
+
+Usage::
+
+    run_test_suite.py [samples_dir]
+
+Plays media samples with the pyglet media_player, recording debug information
+for each sample played and writing reports about the data captured.
+
+Arguments:
+
+.. code-block:: none
+
+    samples_dir: directory with the media samples to play
+
+If no samples_dir is provided the active session is the target.
+If an explicit playlist was specified when creating the session, then only the
+samples in the playlist will be played, otherwise all samples in samples_dir
+will be played.
+
+If sample_dir is provided, a session named ``testrun_00`` (``_01``, ``_02``,
+... if that name was taken) will be created, with no explicit playlist, and
+then the command operates as in the previous case.
+
+Output files will be into:
+
+.. code-block:: none
+
+    samples_dir/session/dbg : binary capture of media_player events, other raw
+                              data captured
+    samples_dir/session/reports : human readable reports
+
+.. note::
+
+    This script will refuse to overwrite an existing ``test_run results``.
+
+Output files will be into subdirectories:
+
+``samples_dir/test_run/dbg``
+
+    Each sample will generate a ``sample.dbg`` file storing the sequence of
+    player debug events seen while playing the sample.
+    It is simply a pickle of a list of tuples, each tuple an event.
+    There are not meant for direct human use, but to run some analyzers to
+    render useful reports.
+
+    A ``crash_retries.pkl`` file, a pickle of
+    ``(total_retries, sometimes_crashed, still_crashing) <-> (int, set, set)``.
+
+    A ``pyglet.info`` captured at session creation to track hw & sw.
+
+    A pyglet hg revision captured at session creation.
+
+``samples_dir/test_run/reports``
+
+    Human readable outputs, described in command ``summarize.py``
+
+    Later a user can generate visualizations and additional reports that will
+    be stored in this directory
+
+
+summarize.py
+------------
+
+Usage::
+
+    summarize.py
+
+Summarizes the session info collected with ``playmany`` and ``retry_crashes``.
+
+A configuration must be active, see command ``configure.py``.
+
+If a playlist was provided at session creation, then only the samples in the
+playlist will be played, otherwise all files in samples_dir.
+
+Produces human readable reports, constructed from the .dbg files.
+
+Output will be in
+
+    ``samples_dir/test_run/reports``
+
+The files in that directory will be
+
+``00_summary.txt`` , which provides:
+
+    - basics defects stats over all samples
+    - a paragraph for each non perfect sample play with the count of each
+      anomaly observed
+
+``03_pyglet_info.txt`` , ``pyglet.info`` output giving OS, python version,
+etc (as captured at session creation).
+
+``04_pyglet_hg_revision.txt`` , pyglet hg revision if running from a repo
+clone, non writen if no repo (as captured at session creation).
+
+``sample_name.all.txt`` and ``sample_name.anomalies.txt`` for each sample that
+played non perfect.
+
+``sample_name.all.txt`` has all info in the ``sample_name.dbg`` in human
+readable form, that is, the sequence of player's internal events along the
+play.
+
+``sample_name.anomalies.txt`` is a reduced version of the ``.all``.
+variant: normal events are not shown, only anomalies.
+
+
+timeline.py
+-----------
+
+Usage::
+
+    timeline.py sample [output_format]
+
+Renders the media player's debug info to a format more suitable to postprocess
+in a spreadsheets or other software, particularly to get a data visualization.
+
+See output details in the manual.
+
+Arguments:
+
+.. code-block:: none
+
+    sample: sample to report
+    output_format : one of { "csv", "pkl"}, by default saves as .pkl (pickle)
+
+The output will be written to session's output dir under
+``reports/sample.timeline.[.pkl or .csv]``.
+
+Example::
+
+    timeline.py small.mp4
+
+will write the output to ``report/small.mp4.timeline.pkl``.
+
+.. note::
+    ``.csv`` sample is currently not implemented.
+
+
+Samples
+=======
+
+Samples should be small, at the moment I suggest an arbitrary 2MB 2 minutes
+limit. The samples dir contains a ``_sources.txt`` which lists from where
+each sample comes.
+
+Caveat:
+
+    Samples are not 'certified to be compliant with the specification'.
+
+    When possible, samples should be played with non ffmpeg software for
+    incidental confirmation of well formed
+
+        ``*.mp4``, ``*.3gp`` played well with Windows Media Player for win7
+
+        ``*.ogv``, ``*. webm`` played well with Firefox 54.0
+
+        ``*.flv``, ``*.mkv`` played well with VLC Media player, but VLC uses
+        ffmpeg
+
+Surely the samples set will be refined as time goes.
+
+
+pycharm notes
+=============
+
+For ``examples/video_ffmpeg`` module visibility and code completion, that
+directory should be a 'content root' in pycharm settings | 'project
+structure'; as projects roots cannot nest, the pyglet working copy cannot be a
+'content root', I removed it; I added also working_copy/pyglet as another
+'content root' so pycharm plays well also en the library proper. This with
+pycharm 2017.2
diff --git a/doc/internal/media_manual.rst b/doc/internal/media_manual.rst
new file mode 100644
index 0000000..4cd7494
--- /dev/null
+++ b/doc/internal/media_manual.rst
@@ -0,0 +1,196 @@
+Media manual
+^^^^^^^^^^^^
+
+Domain knowledge
+================
+
+This tutorial http://dranger.com/ffmpeg/ffmpeg.html is a good intro for
+building some domain knowledge. Bear in mind that the tutorial is rather old,
+and some ffmpeg functions have become deprecated - but the basics are still
+valid.
+
+In the FFmpeg base code there is the ffplay.c player - a very good way to see
+how things are managed. In particular, some newer FFmpeg functions are used,
+while current pyglet media code still uses functions that have now been
+deprecated.
+
+Current code architecture
+=========================
+
+The overview of the media code is the following:
+
+Source
+------
+
+Found in media/sources folder.
+
+:class:`~pyglet.media.Source` s represent data containing media
+information. They can come from disk or be created in memory. A
+:class:`~pyglet.media.Source` 's responsibility is to read data into
+and provide audio and/or video data out of its stream. Essentially, it's a
+*producer*.
+
+FFmpegStreamingSource
+---------------------
+
+One implementation of the :class:`~pyglet.media.StreamingSource` is the
+``FFmpegSource``. It implements the :class:`~pyglet.media.Source` base class
+by calling FFmpeg functions wrapped by ctypes and found in
+media/sources/ffmpeg_lib. They offer basic functionalities for handling media
+streams, such as opening a file, reading stream info, reading a packet, and
+decoding audio and video packets.
+
+The :class:`~pyglet.media.sources.ffmpeg.FFmpegSource` maintains two queues,
+one for audio packets and one for video packets, with a pre-determined maximum
+size. When the source is loaded, it will read packets from the stream and will
+fill up the queues until one of them is full. It has then to stop because we
+never know what type of packet we will get next from the stream. It could be a
+packet of the same type as the filled up queue, in  which case we would not be
+able to store the additional packet.
+
+Whenever a :class:`~pyglet.media.player.Player` - a consumer of a source -
+asks for audio data or a video frame, the
+:class:`~pyglet.media.Source` will pop the next packet from the
+appropriate queue, decode the data, and return the result to the Player. If
+this results in available space in both audio and video queues, it will read
+additional packets until one of the queues is full again.
+
+Player
+------
+
+Found in media/player.py
+
+The :class:`~pyglet.media.player.Player` is the main object that drives the
+source.  It maintains an internal sequence of sources or iterator of sources
+that it can play sequentially. Its responsibilities are to play, pause and seek
+into the source.
+
+If the source contains audio, the :class:`~pyglet.media.player.Player` will
+instantiate an ``AudioPlayer`` by asking the ``SoundDriver`` to create an
+appropriate ``AudioPlayer`` for the given platform. The ``AudioDriver`` is a
+singleton created according to which drivers are available. Currently
+supported sound drivers are: DirectSound, PulseAudio and OpenAL.
+
+If the source contains video, the Player has a
+:meth:`~pyglet.media.Player.get_texture` method returning the current
+video frame.
+
+The player has an internal `master clock` which is used to synchronize the
+video and the audio. The audio synchronization is delegated to the
+``AudioPlayer``. More info found below. The video synchronization is made by
+asking the :class:`~pyglet.media.Source` for the next video timestamp.
+The :class:`~pyglet.media.player.Player` then schedules on pyglet event loop a
+call to its :meth:`~pyglet.media.Player.update_texture` with a delay
+equals to the difference between the next video timestamp and the master clock
+current time.
+
+When :meth:`~pyglet.media.Player.update_texture` is called, we will
+check if the actual master clock time is not too late compared to the video
+timestamp. This could happen if the loop was very busy and the function could
+not be called on time. In this case, the frame would be skipped until we find
+a frame with a suitable timestamp for the current master clock time.
+
+AudioPlayer
+-----------
+
+Found in media/drivers
+
+The ``AudioPlayer`` is responsible only for the audio data. It can read, pause,
+and seek into the :class:`~pyglet.media.Source`.
+
+In order to accomplish these tasks, the audio player keeps a reference to the
+``AudioDriver`` singleton which provides access to the lower level functions
+for the selected audio driver.
+
+When instructed to play, it will register itself on pyglet event loop and
+check every 0.1 seconds if there is enough space in its audio buffer. If so it
+will ask the source for more audio data to refill its audio buffer. It's also
+at this time that it will check for the difference between the estimated audio
+time and the :class:`~pyglet.media.player.Player` master clock. A weighted
+average is used to smooth the inaccuracies of the audio time estimation as
+explained in http://dranger.com/ffmpeg/tutorial06.html. If the resulting
+difference is too big, the ``Source``
+:meth:`~pyglet.media.Source.get_audio_data` method has a
+``compensation_time`` argument which allows it to shorten or stretch the
+number of audio samples. This allows the audio to get back in synch with the
+master clock.
+
+AudioDriver
+-----------
+
+Found in media/drivers
+
+The ``AudioDriver`` is a wrapper around the low-level sound driver available
+on the platform. It's a singleton. It can create an ``AudioPlayer``
+appropriate for the current ``AudioDriver``.
+
+Normal operation of the ``Player``
+----------------------------------
+
+The client code instantiates a media player this way::
+
+    player = pyglet.media.Player()
+    source = pyglet.media.load(filename)
+    player.queue(source)
+    player.play()
+
+When the client code runs ``player.play()``:
+
+The :class:`~pyglet.media.player.Player` will check if there is an audio track
+on the media. If so it will instantiate an ``AudioPlayer`` appropriate for the
+available sound driver on the platform. It will create an empty
+:class:`~pyglet.image.Texture` if the media contains video frames and will
+schedule its :meth:`~pyglet.media.Player.update_texture` to be called
+immediately. Finally it will start the master clock.
+
+The ``AudioPlayer`` will ask its :class:`~pyglet.media.Source` for
+audio data. The :class:`~pyglet.media.Source` will pop the next
+available audio packet and will decode it. The resulting audio data will be
+returned to the ``AudioPlayer``. If the audio queue and the video queues are
+not full, the :class:`~pyglet.media.Source` will read more packets
+from the stream until one of the queues is full again.
+
+When the :meth:`~pyglet.media.Player.update_texture` method is called,
+the next video timestamp will be checked with the master clock. We allow a
+delay up to the frame duration. If the master clock is beyond that time, the
+frame will be skipped. We will check the following frames for its timestamp
+until we find the appropriate frame for the master clock time. We will set the
+:attr:`~pyglet.media.player.Player.texture` to the new video frame. We will
+check for the next video frame timestamp and we will schedule a new call to
+:meth:`~pyglet.media.Player.update_texture` with a delay equals to the
+difference between the next video timestamps and the master clock time.
+
+Helpful tools
+=============
+
+I've found that using the binary ffprobe is a good way to explore the content
+of a media file. Here's a couple of things which might be
+interesting and helpful::
+
+    ffprobe samples_v1.01\SampleVideo_320x240_1mb.3gp -show_frames
+
+This will show information about each frame in the file. You can choose only
+audio or only video frames by using the ``v`` flag for video and ``a`` for
+audio.::
+
+    ffprobe samples_v1.01\SampleVideo_320x240_1mb.3gp -show_frames -select_streams v
+
+
+You can also ask to see a subset of frame information this way::
+
+    ffprobe samples_v1.01\SampleVideo_320x240_1mb.3gp -show_frames
+    -select_streams v -show_entries frame=pkt_pts,pict_type
+
+Finally, you can get a more compact view with the additional ``compact`` flag:
+
+    ffprobe samples_v1.01\SampleVideo_320x240_1mb.3gp -show_frames
+    -select_streams v -show_entries frame=pkt_pts,pict_type -of compact
+
+Convert video to mkv
+====================
+
+::
+
+    ffmpeg -i <original_video> -c:v libx264 -preset slow -profile:v high -crf 18
+    -coder 1 -pix_fmt yuv420p -movflags +faststart -g 30 -bf 2 -c:a aac -b:a 384k
+    -profile:a aac_low <outputfilename.mkv>
diff --git a/doc/internal/testing.rst b/doc/internal/testing.rst
new file mode 100644
index 0000000..f7c2a9d
--- /dev/null
+++ b/doc/internal/testing.rst
@@ -0,0 +1,153 @@
+Testing pyglet
+==============
+
+Test Suites
+-----------
+Tests for pyglet are divided into 3 suites.
+
+Unit tests
+''''''''''
+
+Unit tests only cover a single unit of code or a combination of a
+limited number of units. No resource intensive computations should
+be included. These tests should run in limited time without
+any user interaction.
+
+Integration tests
+'''''''''''''''''
+
+Integration tests cover the integration of components of pyglet
+into the whole of pyglet and the integration into the supported systems.
+Like unit tests these tests do not require user interaction,
+but they can take longer to run due to access to system resources.
+
+Interactive tests
+'''''''''''''''''
+
+Interactive tests require the user to verify whether the test is
+successful and in some cases require the user to perform actions
+in order for the test to continue. These tests can take a
+long time to run.
+
+There are currently 3 types of interactive test cases:
+
+- Tests that can only run in fully interactive mode as they require
+  the user to perform an action in order for the test to continue.
+  These tests are decorated with
+  :func:`~tests.interactive.interactive_test_base.requires_user_action`.
+- Tests that can run without user interaction, but that cannot validate
+  whether they should pass or fail. These tests are docorated with
+  :func:`~tests.interactive.interactive_test_base.requires_user_validation`.
+- Tests that can run without user interaction and that can compare results
+  to screenshots from a previous run to determine whether they pass or
+  fail. This is the default type.
+
+Running tests
+-------------
+
+The pyglet test suite is based on the `pytest framework <http://pytest.org>`_.
+
+It is preferred to use a virtual environment to run the tests.
+For instructions to set up virtual environments see :doc:`virtualenv`.
+Make sure the virtual environment for the Python version you want to
+test is active. It is preferred to run the tests on 3.6+
+to make sure changes are compatible with all supported Python versions.
+
+To run all tests, execute pytest in the root of the pyglet repository::
+
+    pytest
+
+You can also run just a single suite::
+
+    pytest tests/unit
+    pytest tests/integration
+    pytest tests/interactive
+
+For the interactive test suites, there are some extra command line switches
+for pytest:
+
+- ``--non-interactive``: Only run the interactive tests that can only
+  verify themselves using screenshots. The screenshots are created when
+  you run the tests in interactive mode, so you will need to run the tests
+  interactively once, before you can use this option;
+- ``--sanity``: Do a sanity check by running as many interactive tests
+  without user intervention. Not all tests can run without intervention,
+  so these tests will still be skipped. Mostly useful to quickly check
+  changes in code. Not all tests perform complete validation.
+
+Writing tests
+-------------
+
+Annotations
+'''''''''''
+
+Some control over test execution can be exerted by using annotations in
+the form of decorators. One function of annotations is to skip tests
+under certain conditions.
+
+General annotations
+^^^^^^^^^^^^^^^^^^^
+
+General test annotations are available in the module :mod:`tests.annotations`.
+
+.. py:currentmodule:: tests.annotations
+.. py:decorator:: require_platform(platform)
+
+   Only run the test on the given platform(s), skip on other platforms.
+
+   :param list(str) platform: A list of platform identifiers as returned by
+       :data:`pyglet.options`. See also :class:`~tests.annotations.Platform`.
+
+.. py:decorator:: skip_platform(platform)
+
+   Skip test on the given platform(s).
+
+   :param list(str) platform: A list of platform identifiers as returned by
+       :data:`pyglet.options`. See also :class:`~tests.annotations.Platform`.
+
+.. autoclass:: tests.annotations.Platform
+   :members:
+
+.. py:decorator:: require_gl_extension(extension)
+
+   Skip the test if the given GL extension is not available.
+
+   :param str extension: Name of the extension required.
+
+Suite annotations
+^^^^^^^^^^^^^^^^^
+
+This is currently not used.
+
+.. py:decorator:: pytest.mark.unit
+
+   Test belongs to the unit test suite.
+
+.. py:decorator:: pytest.mark.integration
+
+   Test belongs to the integration test suite.
+
+.. py:decorator:: pytest.mark.interactive
+
+  Test belongs to the interactive test suite.
+
+Interactive test annotations
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Interactive test cases can be marked with specific pytest marks. Currently
+the following marks are used:
+
+.. py:decorator:: pytest.mark.requires_user_action
+
+   Test requires user interaction to run. It needs to be skipped when running in
+   non-interactive or sanity mode.
+
+.. py:decorator:: pytest.mark.requires_user_validation
+
+   User validation is required to mark the test passed or failed. However the test
+   can run in sanity mode.
+
+.. py:decorator:: pytest.mark.only_interactive
+
+   For another reason the test can only run in interactive mode.
+
diff --git a/doc/internal/virtualenv.rst b/doc/internal/virtualenv.rst
new file mode 100644
index 0000000..2da5b05
--- /dev/null
+++ b/doc/internal/virtualenv.rst
@@ -0,0 +1,148 @@
+Development environment
+=======================
+
+To develop pyglet, you need an environment with at least the following:
+
+    - Python 3.6+
+    - `pytest <https://pytest.org>`_
+    - Your favorite Python editor or IDE
+
+All requirements should already be located in ``doc/requirements.txt``
+and ``tests/requirements.txt``.
+
+.. code::
+
+    pip install -r doc/requirements.txt
+    pip install -r tests/requirements.txt
+
+To use and test all pyglet functionality you should also have:
+
+    - `FFmpeg <https://www.ffmpeg.org/download.html>`_
+    - `Pillow <https://pillow.readthedocs.io>`_
+    - `coverage <https://coverage.readthedocs.io>`_
+
+To build packages for distribution you need to install:
+
+    - `wheel <https://github.com/pypa/wheel/>`_
+
+It is preferred to create a Python virtual environment to develop in.
+This allows you to easily test on all Python versions supported by pyglet,
+not pollute your local system with pyglet development dependencies,
+and not have your local system interfere with pyglet developement.
+All dependencies you install while inside an activated virtual
+environment will remain isolated inside that environment.
+When you're finished, you can simply delete it.
+
+This section will show you how to set up and use virtual environments.
+If you're already familiar with this, you can probably skip the rest of
+this page.
+
+Linux or Mac OSX
+----------------
+
+Setting up
+''''''''''
+
+Setting up a virtual environment is almost the same for Linux and OS X.
+First, use your OS's package manager (apt, brew, etc) to install the
+following dependencies:
+
+    - Python 3.6+
+
+To create virtual environments, ``venv`` is included in the standard
+library since Python 3.3.
+
+Depending on your platform, python may be installed as ``python`` or ``python3``.
+You may want to check which command runs python 3 on your system::
+
+    python --version
+    python3 --version
+
+For the rest of the guide, use whichever gives you the correct python version on your system.
+Some linux distros may install python with version numbers such as `python3.6`, so you may need
+to set up an alias.
+
+Next, we'll create a virtual environment.
+Choose the appropriate command for your system to create a virtual environment::
+
+    python -m venv pyglet-venv
+    python3 -m venv pyglet-venv
+
+Once the virtual environment has been created, the next step is to activate
+it. You'll then install the dependencies, which will be isolated
+inside that virtual environment.
+
+Activate the virtual environment ::
+
+   . pyglet-venv/bin/activate
+
+You will see the name of the virtual environment at the start of the
+command prompt.
+
+[Optional] Make sure pip is the latest version::
+
+    pip install --upgrade pip
+
+Now install dependencies in ``doc/requirements.txt`` and
+``tests/requirements.txt``::
+
+    pip install -r doc/requirements.txt
+    pip install -r tests/requirements.txt
+
+Finishing
+'''''''''
+
+To get out of the virtual environment run::
+
+   deactivate
+
+Windows
+-------
+
+Setting up
+''''''''''
+
+Make sure you download and install:
+
+    - `Python 3.6+  <http://www.python.org/downloads/windows/>`_
+
+Pip should be installed by default with the latest Python installers.
+Make sure that the boxes for installing PIP and adding python to PATH are checked.
+
+When finished installing, open a command prompt.
+
+To create virtual environments, ``venv`` is included in the standard library
+since Python 3.3.
+
+Next, we'll create a virtual environment.::
+
+    python -m venv pyglet-venv
+
+Once the virtual environment has been created, the next step is to activate
+it. You'll then install the dependencies, which will be isolated
+inside that virtual environment.
+
+Activate the virtual environment ::
+
+   . pyglet-venv/bin/activate
+
+You will see the name of the virtual environment at the start of the
+command prompt.
+
+[Optional] Make sure pip is the latest version::
+
+   pip install --upgrade pip
+
+
+Now install dependencies in ``doc/requirements.txt`` and
+``tests/requirements.txt``::
+
+    pip install -r doc/requirements.txt
+    pip install -r tests/requirements.txt
+
+Finishing
+'''''''''
+
+To get out of the virtual environment run::
+
+   deactivate
diff --git a/doc/internal/wraptypes-class.svg b/doc/internal/wraptypes-class.svg
new file mode 100644
index 0000000..745e24d
--- /dev/null
+++ b/doc/internal/wraptypes-class.svg
@@ -0,0 +1,2459 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   style="fill-opacity:1; color-rendering:auto; color-interpolation:auto; text-rendering:auto; stroke:black; stroke-linecap:square; stroke-miterlimit:10; shape-rendering:auto; stroke-opacity:1; fill:black; stroke-dasharray:none; font-weight:normal; stroke-width:1; font-family:'Dialog'; font-style:normal; stroke-linejoin:miter; font-size:12; stroke-dashoffset:0; image-rendering:auto;"
+   width="966"
+   height="696"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.44.1"
+   sodipodi:docname="Class Diagram1.svg"
+   sodipodi:docbase="/home/alex">
+  <metadata
+     id="metadata871">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview
+     inkscape:window-height="638"
+     inkscape:window-width="858"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     guidetolerance="10.0"
+     gridtolerance="10.0"
+     objecttolerance="10.0"
+     borderopacity="1.0"
+     bordercolor="#666666"
+     pagecolor="#ffffff"
+     id="base"
+     inkscape:zoom="0.5862069"
+     inkscape:cx="679.08734"
+     inkscape:cy="265.50823"
+     inkscape:window-x="1632"
+     inkscape:window-y="0"
+     inkscape:current-layer="svg2" />
+<!--Generated by the Batik Graphics2D SVG Generator-->  <defs
+     id="genericDefs">
+    <defs
+       id="defs1">
+      <clipPath
+         id="clipPath1"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path9"
+           d="M -3,-3 L 637,-3 L 637,307 L -3,307 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath2"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path12"
+           d="M 0,0 L 0,14 L 630,14 L 630,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath3"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path15"
+           d="M -3,-3 L 217,-3 L 217,117 L -3,117 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath4"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path18"
+           d="M 0,0 L 0,103 L 210,103 L 210,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath5"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path21"
+           d="M 0,0 L 0,14 L 210,14 L 210,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath6"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path24"
+           d="M -3,-3 L 47,-3 L 47,27 L -3,27 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath7"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path27"
+           d="M -3,-3 L 137,-3 L 137,77 L -3,77 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath8"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path30"
+           d="M 0,0 L 0,63 L 130,63 L 130,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath9"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path33"
+           d="M 0,0 L 0,14 L 130,14 L 130,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath10"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path36"
+           d="M -3,-3 L 117,-3 L 117,47 L -3,47 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath11"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path39"
+           d="M 0,0 L 0,33 L 110,33 L 110,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath12"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path42"
+           d="M 0,0 L 0,14 L 110,14 L 110,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath13"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path45"
+           d="M -3,-3 L 137,-3 L 137,47 L -3,47 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath14"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path48"
+           d="M 0,0 L 0,33 L 130,33 L 130,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath15"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path51"
+           d="M -3,-3 L 177,-3 L 177,47 L -3,47 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath16"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path54"
+           d="M 0,0 L 0,33 L 170,33 L 170,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath17"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path57"
+           d="M 0,0 L 0,14 L 170,14 L 170,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath18"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path60"
+           d="M -3,-3 L 267,-3 L 267,137 L -3,137 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath19"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path63"
+           d="M 0,0 L 0,123 L 260,123 L 260,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath20"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path66"
+           d="M 0,0 L 0,14 L 260,14 L 260,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath21"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path69"
+           d="M -3,-3 L 357,-3 L 357,267 L -3,267 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath22"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path72"
+           d="M 0,0 L 0,14 L 350,14 L 350,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath23"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path75"
+           d="M -3,-3 L 567,-3 L 567,387 L -3,387 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath24"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path78"
+           d="M 0,0 L 0,14 L 560,14 L 560,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath25"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path81"
+           d="M -3,-3 L 237,-3 L 237,67 L -3,67 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath26"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path84"
+           d="M 0,0 L 0,53 L 230,53 L 230,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath27"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path87"
+           d="M 0,0 L 0,14 L 230,14 L 230,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath28"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path90"
+           d="M -3,-3 L 137,-3 L 137,57 L -3,57 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath29"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path93"
+           d="M 0,0 L 0,43 L 130,43 L 130,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath30"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path96"
+           d="M -3,-3 L 107,-3 L 107,47 L -3,47 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath31"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path99"
+           d="M 0,0 L 0,33 L 100,33 L 100,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath32"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path102"
+           d="M 0,0 L 0,14 L 100,14 L 100,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath33"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path105"
+           d="M -3,-3 L 97,-3 L 97,47 L -3,47 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath34"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path108"
+           d="M 0,0 L 0,33 L 90,33 L 90,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath35"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path111"
+           d="M 0,0 L 0,14 L 90,14 L 90,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath36"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path114"
+           d="M -3,-3 L 107,-3 L 107,87 L -3,87 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath37"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path117"
+           d="M 0,0 L 0,73 L 100,73 L 100,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath38"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path120"
+           d="M -3,-3 L 97,-3 L 97,57 L -3,57 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath39"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path123"
+           d="M 0,0 L 0,43 L 90,43 L 90,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath40"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path126"
+           d="M -3,-3 L 197,-3 L 197,87 L -3,87 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath41"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path129"
+           d="M 0,0 L 0,73 L 190,73 L 190,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath42"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path132"
+           d="M 0,0 L 0,14 L 190,14 L 190,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath43"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path135"
+           d="M -3,-3 L 87,-3 L 87,47 L -3,47 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath44"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path138"
+           d="M 0,0 L 0,33 L 80,33 L 80,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath45"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path141"
+           d="M 0,0 L 0,14 L 80,14 L 80,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath46"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path144"
+           d="M -3,-3 L 157,-3 L 157,197 L -3,197 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath47"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path147"
+           d="M 0,0 L 0,183 L 150,183 L 150,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath48"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path150"
+           d="M 0,0 L 0,14 L 150,14 L 150,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath49"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path153"
+           d="M -3,-3 L 147,-3 L 147,47 L -3,47 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath50"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path156"
+           d="M 0,0 L 0,33 L 140,33 L 140,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath51"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path159"
+           d="M 0,0 L 0,14 L 140,14 L 140,0 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath52"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path162"
+           d="M -3,-3 L 127,-3 L 127,27 L -3,27 L -3,-3 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath53"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path165"
+           d="M 0,0 L 40,0 L 40,20 L 0,20 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath54"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path168"
+           d="M 0,0 L 80,0 L 80,14 L 0,14 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath55"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path171"
+           d="M 0,0 L 45,0 L 45,14 L 0,14 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath56"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path174"
+           d="M 0,0 L 59,0 L 59,14 L 0,14 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath57"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path177"
+           d="M 0,0 L 66,0 L 66,14 L 0,14 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath58"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path180"
+           d="M 0,0 L 120,0 L 120,20 L 0,20 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath59"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path183"
+           d="M 0,0 L 153,0 L 153,104 L 0,104 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath60"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path186"
+           d="M 0,0 L 104,0 L 104,243 L 0,243 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath61"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path189"
+           d="M 0,0 L 133,0 L 133,104 L 0,104 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath62"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path192"
+           d="M 0,0 L 154,0 L 154,223 L 0,223 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath63"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path195"
+           d="M 0,0 L 244,0 L 244,223 L 0,223 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath64"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path198"
+           d="M 0,0 L 104,0 L 104,183 L 0,183 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath65"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path201"
+           d="M 0,0 L 194,0 L 194,213 L 0,213 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath66"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path204"
+           d="M 0,0 L 413,0 L 413,184 L 0,184 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath67"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path207"
+           d="M 0,0 L 104,0 L 104,223 L 0,223 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath68"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path210"
+           d="M 0,0 L 144,0 L 144,243 L 0,243 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath69"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path213"
+           d="M 0,0 L 263,0 L 263,253 L 0,253 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath70"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path216"
+           d="M 0,0 L 233,0 L 233,243 L 0,243 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath71"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path219"
+           d="M 0,0 L 163,0 L 163,204 L 0,204 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath72"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path222"
+           d="M 0,0 L 143,0 L 143,134 L 0,134 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath73"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path225"
+           d="M 0,0 L 233,0 L 233,104 L 0,104 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath74"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path228"
+           d="M 0,0 L 163,0 L 163,104 L 0,104 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath75"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path231"
+           d="M 0,0 L 143,0 L 143,104 L 0,104 L 0,0 z " />
+      </clipPath>
+      <clipPath
+         id="clipPath76"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path234"
+           d="M 0,0 L 104,0 L 104,413 L 0,413 L 0,0 z " />
+      </clipPath>
+    </defs>
+  </defs>
+  <g
+     id="g236"
+     style="fill:white;stroke:white"
+     transform="translate(3.411765,2.558783)">
+    <rect
+       id="rect238"
+       height="696"
+       style="stroke:none"
+       width="966"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g240"
+     style="font-size:11px;fill:#7acff5;stroke:#7acff5;font-family:sans-serif"
+     transform="translate(145.4118,4.558783)">
+    <rect
+       id="rect242"
+       height="20"
+       style="stroke:none"
+       width="125"
+       y="0"
+       x="0" />
+    <rect
+       id="rect244"
+       height="279"
+       style="stroke:none"
+       width="629"
+       y="20"
+       x="0" />
+  </g>
+  <g
+     id="g246"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(145.4118,4.558783)">
+    <rect
+       id="rect248"
+       height="20"
+       style="fill:none"
+       width="125"
+       y="0"
+       x="0" />
+    <rect
+       id="rect250"
+       height="279"
+       style="fill:none"
+       width="629"
+       y="20"
+       x="0" />
+  </g>
+  <g
+     id="g252"
+     transform="translate(145.4118,24.55878)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text254"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="278">preprocessor</text>
+  </g>
+  <g
+     id="g256"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(155.4118,114.5588)">
+    <rect
+       id="rect258"
+       height="110"
+       style="stroke:none"
+       width="210"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g260"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(155.4118,114.5588)">
+    <rect
+       id="rect262"
+       height="110"
+       style="fill:none"
+       width="210"
+       y="0"
+       x="0" />
+    <line
+       id="line264"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="210"
+       x1="0" />
+  </g>
+  <g
+     id="g266"
+     transform="translate(155.4118,128.5588)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text268"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+parse(filename, data, namespace)</text>
+    <text
+       id="text270"
+       xml:space="preserve"
+       style="stroke:none"
+       y="27"
+       x="2">+include(header)</text>
+    <text
+       id="text272"
+       xml:space="preserve"
+       style="stroke:none"
+       y="41"
+       x="2">+include_system(header)</text>
+    <text
+       id="text274"
+       xml:space="preserve"
+       style="stroke:none"
+       y="55"
+       x="2">+include_next(header, ref)</text>
+    <text
+       id="text276"
+       xml:space="preserve"
+       style="stroke:none"
+       y="69"
+       x="2">+import_(header)</text>
+    <text
+       id="text278"
+       xml:space="preserve"
+       style="stroke:none"
+       y="83"
+       x="2">+import_system(header)</text>
+  </g>
+  <g
+     id="g280"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(155.4118,114.5588)">
+    <text
+       id="text282"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="51">PreprocessorParser</text>
+  </g>
+  <g
+     id="g284"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(425.4118,194.5588)">
+    <rect
+       id="rect286"
+       height="70"
+       style="stroke:none"
+       width="130"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g288"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(425.4118,194.5588)">
+    <rect
+       id="rect290"
+       height="70"
+       style="fill:none"
+       width="130"
+       y="0"
+       x="0" />
+    <line
+       id="line292"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="130"
+       x1="0" />
+  </g>
+  <g
+     id="g294"
+     transform="translate(425.4118,208.5588)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text296"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+push_input()</text>
+    <text
+       id="text298"
+       xml:space="preserve"
+       style="stroke:none"
+       y="27"
+       x="2">+pop_input()</text>
+    <text
+       id="text300"
+       xml:space="preserve"
+       style="stroke:none"
+       y="41"
+       x="2">+token()</text>
+  </g>
+  <g
+     id="g302"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(425.4118,194.5588)">
+    <text
+       id="text304"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="13">PreprocessorLexer</text>
+  </g>
+  <g
+     id="g306"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(365.4118,54.55878)">
+    <rect
+       id="rect308"
+       height="40"
+       style="stroke:none"
+       width="110"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g310"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(365.4118,54.55878)">
+    <rect
+       id="rect312"
+       height="40"
+       style="fill:none"
+       width="110"
+       y="0"
+       x="0" />
+    <line
+       id="line314"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="110"
+       x1="0" />
+  </g>
+  <g
+     id="g316"
+     transform="translate(365.4118,68.55878)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text318"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+token()</text>
+  </g>
+  <g
+     id="g320"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(365.4118,54.55878)">
+    <text
+       id="text322"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="12">TokenListLexer</text>
+  </g>
+  <g
+     id="g324"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(5.411765,14.55878)">
+    <rect
+       id="rect326"
+       height="40"
+       style="stroke:none"
+       width="130"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g328"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(5.411765,14.55878)">
+    <rect
+       id="rect330"
+       height="40"
+       style="fill:none"
+       width="130"
+       y="0"
+       x="0" />
+    <line
+       id="line332"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="130"
+       x1="0" />
+  </g>
+  <g
+     id="g334"
+     transform="translate(5.411765,28.55878)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text336"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+parse(filename, data)</text>
+  </g>
+  <g
+     id="g338"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(5.411765,14.55878)">
+    <text
+       id="text340"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="34">yacc.Parser</text>
+  </g>
+  <g
+     id="g342"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(155.4118,54.55878)">
+    <rect
+       id="rect344"
+       height="40"
+       style="stroke:none"
+       width="170"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g346"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(155.4118,54.55878)">
+    <rect
+       id="rect348"
+       height="40"
+       style="fill:none"
+       width="170"
+       y="0"
+       x="0" />
+    <line
+       id="line350"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="170"
+       x1="0" />
+  </g>
+  <g
+     id="g352"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(155.4118,54.55878)">
+    <text
+       id="text354"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="12">ConstantExpressionParser</text>
+  </g>
+  <g
+     id="g356"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(495.4118,54.55878)">
+    <rect
+       id="rect358"
+       height="130"
+       style="stroke:none"
+       width="260"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g360"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(495.4118,54.55878)">
+    <rect
+       id="rect362"
+       height="130"
+       style="fill:none"
+       width="260"
+       y="0"
+       x="0" />
+    <line
+       id="line364"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="260"
+       x1="0" />
+  </g>
+  <g
+     id="g366"
+     transform="translate(495.4118,68.55878)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text368"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+objects</text>
+    <text
+       id="text370"
+       xml:space="preserve"
+       style="stroke:none"
+       y="27"
+       x="2">+functions</text>
+    <line
+       id="line372"
+       y2="32"
+       style="fill:none;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0"
+       y1="32"
+       x2="260"
+       x1="0" />
+    <text
+       id="text374"
+       xml:space="preserve"
+       style="stroke:none"
+       y="45"
+       x="2">+define_object(name, replacements)</text>
+    <text
+       id="text376"
+       xml:space="preserve"
+       style="stroke:none"
+       y="59"
+       x="2">+define_function(name, params, replacements)</text>
+    <text
+       id="text378"
+       xml:space="preserve"
+       style="stroke:none"
+       y="73"
+       x="2">+undef(name)</text>
+    <text
+       id="text380"
+       xml:space="preserve"
+       style="stroke:none"
+       y="87"
+       x="2">+is_defined(name)</text>
+    <text
+       id="text382"
+       xml:space="preserve"
+       style="stroke:none"
+       y="101"
+       x="2">+apply_macros(tokens)</text>
+  </g>
+  <g
+     id="g384"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(495.4118,54.55878)">
+    <text
+       id="text386"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="62">PreprocessorNamespace</text>
+  </g>
+  <g
+     id="g388"
+     style="font-size:11px;fill:#7acff5;stroke:#7acff5;font-family:sans-serif"
+     transform="translate(35.41176,314.5588)">
+    <rect
+       id="rect390"
+       height="20"
+       style="stroke:none"
+       width="69"
+       y="0"
+       x="0" />
+    <rect
+       id="rect392"
+       height="239"
+       style="stroke:none"
+       width="349"
+       y="20"
+       x="0" />
+  </g>
+  <g
+     id="g394"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(35.41176,314.5588)">
+    <rect
+       id="rect396"
+       height="20"
+       style="fill:none"
+       width="69"
+       y="0"
+       x="0" />
+    <rect
+       id="rect398"
+       height="239"
+       style="fill:none"
+       width="349"
+       y="20"
+       x="0" />
+  </g>
+  <g
+     id="g400"
+     transform="translate(35.41176,334.5588)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text402"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="154">cparser</text>
+  </g>
+  <g
+     id="g404"
+     style="font-size:11px;fill:#7acff5;stroke:#7acff5;font-family:sans-serif"
+     transform="translate(405.4118,314.5588)">
+    <rect
+       id="rect406"
+       height="20"
+       style="stroke:none"
+       width="111"
+       y="0"
+       x="0" />
+    <rect
+       id="rect408"
+       height="359"
+       style="stroke:none"
+       width="559"
+       y="20"
+       x="0" />
+  </g>
+  <g
+     id="g410"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(405.4118,314.5588)">
+    <rect
+       id="rect412"
+       height="20"
+       style="fill:none"
+       width="111"
+       y="0"
+       x="0" />
+    <rect
+       id="rect414"
+       height="359"
+       style="fill:none"
+       width="559"
+       y="20"
+       x="0" />
+  </g>
+  <g
+     id="g416"
+     transform="translate(405.4118,334.5588)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text418"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="245">ctypesparser</text>
+  </g>
+  <g
+     id="g420"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(615.4118,354.5588)">
+    <rect
+       id="rect422"
+       height="60"
+       style="stroke:none"
+       width="230"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g424"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(615.4118,354.5588)">
+    <rect
+       id="rect426"
+       height="60"
+       style="fill:none"
+       width="230"
+       y="0"
+       x="0" />
+    <line
+       id="line428"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="230"
+       x1="0" />
+  </g>
+  <g
+     id="g430"
+     transform="translate(615.4118,368.5588)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text432"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+name</text>
+    <line
+       id="line434"
+       y2="18"
+       style="fill:none;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0"
+       y1="18"
+       x2="230"
+       x1="0" />
+    <text
+       id="text436"
+       xml:space="preserve"
+       style="stroke:none"
+       y="31"
+       x="2">+visit(visitor)</text>
+  </g>
+  <g
+     id="g438"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(615.4118,354.5588)">
+    <text
+       id="text440"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="83">CtypesType</text>
+  </g>
+  <g
+     id="g442"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(715.4118,594.5588)">
+    <rect
+       id="rect444"
+       height="50"
+       style="stroke:none"
+       width="130"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g446"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(715.4118,594.5588)">
+    <rect
+       id="rect448"
+       height="50"
+       style="fill:none"
+       width="130"
+       y="0"
+       x="0" />
+    <line
+       id="line450"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="130"
+       x1="0" />
+  </g>
+  <g
+     id="g452"
+     transform="translate(715.4118,608.5588)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text454"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+visit_struct(struct)</text>
+    <text
+       id="text456"
+       xml:space="preserve"
+       style="stroke:none"
+       y="27"
+       x="2">+visit_enum(enum)</text>
+  </g>
+  <g
+     id="g458"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(715.4118,594.5588)">
+    <text
+       id="text460"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="13">CtypesTypeVisitor</text>
+  </g>
+  <g
+     id="g462"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(755.4118,534.5588)">
+    <rect
+       id="rect464"
+       height="40"
+       style="stroke:none"
+       width="100"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g466"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(755.4118,534.5588)">
+    <rect
+       id="rect468"
+       height="40"
+       style="fill:none"
+       width="100"
+       y="0"
+       x="0" />
+    <line
+       id="line470"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="100"
+       x1="0" />
+  </g>
+  <g
+     id="g472"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(755.4118,534.5588)">
+    <text
+       id="text474"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="11">CtypesPointer</text>
+  </g>
+  <g
+     id="g476"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(865.4118,534.5588)">
+    <rect
+       id="rect478"
+       height="40"
+       style="stroke:none"
+       width="90"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g480"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(865.4118,534.5588)">
+    <rect
+       id="rect482"
+       height="40"
+       style="fill:none"
+       width="90"
+       y="0"
+       x="0" />
+    <line
+       id="line484"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="90"
+       x1="0" />
+  </g>
+  <g
+     id="g486"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(865.4118,534.5588)">
+    <text
+       id="text488"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="11">CtypesArray</text>
+  </g>
+  <g
+     id="g490"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(425.4118,534.5588)">
+    <rect
+       id="rect492"
+       height="40"
+       style="stroke:none"
+       width="110"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g494"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(425.4118,534.5588)">
+    <rect
+       id="rect496"
+       height="40"
+       style="fill:none"
+       width="110"
+       y="0"
+       x="0" />
+    <line
+       id="line498"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="110"
+       x1="0" />
+  </g>
+  <g
+     id="g500"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(425.4118,534.5588)">
+    <text
+       id="text502"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="12">CtypesFunction</text>
+  </g>
+  <g
+     id="g504"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(645.4118,494.5588)">
+    <rect
+       id="rect506"
+       height="80"
+       style="stroke:none"
+       width="100"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g508"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(645.4118,494.5588)">
+    <rect
+       id="rect510"
+       height="80"
+       style="fill:none"
+       width="100"
+       y="0"
+       x="0" />
+    <line
+       id="line512"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="100"
+       x1="0" />
+  </g>
+  <g
+     id="g514"
+     transform="translate(645.4118,508.5588)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text516"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+tag</text>
+    <text
+       id="text518"
+       xml:space="preserve"
+       style="stroke:none"
+       y="27"
+       x="2">+is_union</text>
+    <text
+       id="text520"
+       xml:space="preserve"
+       style="stroke:none"
+       y="41"
+       x="2">+members</text>
+    <text
+       id="text522"
+       xml:space="preserve"
+       style="stroke:none"
+       y="55"
+       x="2">+opaque</text>
+  </g>
+  <g
+     id="g524"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(645.4118,494.5588)">
+    <text
+       id="text526"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="15">CtypesStruct</text>
+  </g>
+  <g
+     id="g528"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(545.4118,524.5588)">
+    <rect
+       id="rect530"
+       height="50"
+       style="stroke:none"
+       width="90"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g532"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(545.4118,524.5588)">
+    <rect
+       id="rect534"
+       height="50"
+       style="fill:none"
+       width="90"
+       y="0"
+       x="0" />
+    <line
+       id="line536"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="90"
+       x1="0" />
+  </g>
+  <g
+     id="g538"
+     transform="translate(545.4118,538.5588)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text540"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+tag</text>
+    <text
+       id="text542"
+       xml:space="preserve"
+       style="stroke:none"
+       y="27"
+       x="2">+enumerators</text>
+  </g>
+  <g
+     id="g544"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(545.4118,524.5588)">
+    <text
+       id="text546"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="10">CtypesEnum</text>
+  </g>
+  <g
+     id="g548"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(425.4118,594.5588)">
+    <rect
+       id="rect550"
+       height="80"
+       style="stroke:none"
+       width="190"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g552"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(425.4118,594.5588)">
+    <rect
+       id="rect554"
+       height="80"
+       style="fill:none"
+       width="190"
+       y="0"
+       x="0" />
+    <line
+       id="line556"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="190"
+       x1="0" />
+  </g>
+  <g
+     id="g558"
+     transform="translate(425.4118,608.5588)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text560"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+handle_ctypes_constant()</text>
+    <text
+       id="text562"
+       xml:space="preserve"
+       style="stroke:none"
+       y="27"
+       x="2">+handle_ctypes_type_definition()</text>
+    <text
+       id="text564"
+       xml:space="preserve"
+       style="stroke:none"
+       y="41"
+       x="2">+handle_ctypes_function()</text>
+    <text
+       id="text566"
+       xml:space="preserve"
+       style="stroke:none"
+       y="55"
+       x="2">+handle_ctypes_variable()</text>
+  </g>
+  <g
+     id="g568"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(425.4118,594.5588)">
+    <text
+       id="text570"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="59">CtypesParser</text>
+  </g>
+  <g
+     id="g572"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(255.4118,454.5588)">
+    <rect
+       id="rect574"
+       height="40"
+       style="stroke:none"
+       width="80"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g576"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(255.4118,454.5588)">
+    <rect
+       id="rect578"
+       height="40"
+       style="fill:none"
+       width="80"
+       y="0"
+       x="0" />
+    <line
+       id="line580"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="80"
+       x1="0" />
+  </g>
+  <g
+     id="g582"
+     transform="translate(255.4118,468.5588)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text584"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+token()</text>
+  </g>
+  <g
+     id="g586"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(255.4118,454.5588)">
+    <text
+       id="text588"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="21">CLexer</text>
+  </g>
+  <g
+     id="g590"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(55.41176,364.5588)">
+    <rect
+       id="rect592"
+       height="190"
+       style="stroke:none"
+       width="150"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g594"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(55.41176,364.5588)">
+    <rect
+       id="rect596"
+       height="190"
+       style="fill:none"
+       width="150"
+       y="0"
+       x="0" />
+    <line
+       id="line598"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="150"
+       x1="0" />
+  </g>
+  <g
+     id="g600"
+     transform="translate(55.41176,378.5588)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text602"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+parse(filename, data)</text>
+    <text
+       id="text604"
+       xml:space="preserve"
+       style="stroke:none"
+       y="27"
+       x="2">+handle_include()</text>
+    <text
+       id="text606"
+       xml:space="preserve"
+       style="stroke:none"
+       y="41"
+       x="2">+handle_define()</text>
+    <text
+       id="text608"
+       xml:space="preserve"
+       style="stroke:none"
+       y="55"
+       x="2">+handle_undef()</text>
+    <text
+       id="text610"
+       xml:space="preserve"
+       style="stroke:none"
+       y="69"
+       x="2">+handle_define_constant()</text>
+    <text
+       id="text612"
+       xml:space="preserve"
+       style="stroke:none"
+       y="83"
+       x="2">+handle_if()</text>
+    <text
+       id="text614"
+       xml:space="preserve"
+       style="stroke:none"
+       y="97"
+       x="2">+handle_ifdef()</text>
+    <text
+       id="text616"
+       xml:space="preserve"
+       style="stroke:none"
+       y="111"
+       x="2">+handle_ifndef()</text>
+    <text
+       id="text618"
+       xml:space="preserve"
+       style="stroke:none"
+       y="125"
+       x="2">+handle_elif()</text>
+    <text
+       id="text620"
+       xml:space="preserve"
+       style="stroke:none"
+       y="139"
+       x="2">+handle_else()</text>
+    <text
+       id="text622"
+       xml:space="preserve"
+       style="stroke:none"
+       y="153"
+       x="2">+handle_endif()</text>
+    <text
+       id="text624"
+       xml:space="preserve"
+       style="stroke:none"
+       y="167"
+       x="2">+handle_declaration()</text>
+  </g>
+  <g
+     id="g626"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(55.41176,364.5588)">
+    <text
+       id="text628"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="53">CParser</text>
+  </g>
+  <g
+     id="g630"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(235.4118,364.5588)">
+    <rect
+       id="rect632"
+       height="40"
+       style="stroke:none"
+       width="140"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g634"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(235.4118,364.5588)">
+    <rect
+       id="rect636"
+       height="40"
+       style="fill:none"
+       width="140"
+       y="0"
+       x="0" />
+    <line
+       id="line638"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="140"
+       x1="0" />
+  </g>
+  <g
+     id="g640"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(235.4118,364.5588)">
+    <text
+       id="text642"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="12">CPreprocessorParser</text>
+  </g>
+  <g
+     id="g644"
+     transform="translate(565.4118,394.5588)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text646"
+       xml:space="preserve"
+       style="stroke:none"
+       y="14"
+       x="17">*</text>
+  </g>
+  <g
+     id="g648"
+     transform="translate(795.4118,503.5588)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text650"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="10">destination</text>
+  </g>
+  <g
+     id="g652"
+     transform="translate(905.4118,503.5588)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text654"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="10">base</text>
+  </g>
+  <g
+     id="g656"
+     transform="translate(474.4118,363.5588)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text658"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="9">restype</text>
+  </g>
+  <g
+     id="g660"
+     transform="translate(494.4118,403.5588)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text662"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="9">argtypes</text>
+  </g>
+  <g
+     id="g664"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(155.4118,424.5588)">
+    <line
+       id="line666"
+       y2="50"
+       style="fill:none"
+       y1="50"
+       x2="99"
+       x1="50" />
+  </g>
+  <g
+     id="g668"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(255.4118,174.5588)">
+    <line
+       id="line670"
+       y2="189"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="50" />
+    <polygon
+       id="polygon672"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon674"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g676"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(155.4118,344.5588)">
+    <line
+       id="line678"
+       y2="50"
+       style="fill:none"
+       y1="50"
+       x2="79"
+       x1="50" />
+    <line
+       id="line680"
+       y2="44"
+       style="fill:none"
+       y1="56"
+       x2="63"
+       x1="54" />
+    <line
+       id="line682"
+       y2="56"
+       style="fill:none"
+       y1="44"
+       x2="63"
+       x1="54" />
+    <line
+       id="line684"
+       y2="44"
+       style="fill:none"
+       y1="50"
+       x2="67"
+       x1="79" />
+    <line
+       id="line686"
+       y2="56"
+       style="fill:none"
+       y1="50"
+       x2="67"
+       x1="79" />
+  </g>
+  <g
+     id="g688"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(675.4118,364.5588)">
+    <line
+       id="line690"
+       y2="100"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="50" />
+    <line
+       id="line692"
+       y2="100"
+       style="fill:none"
+       y1="100"
+       x2="100"
+       x1="50" />
+    <line
+       id="line694"
+       y2="169"
+       style="fill:none"
+       y1="100"
+       x2="100"
+       x1="100" />
+    <polygon
+       id="polygon696"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon698"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g700"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(775.4118,364.5588)">
+    <line
+       id="line702"
+       y2="120"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="50" />
+    <line
+       id="line704"
+       y2="120"
+       style="fill:none"
+       y1="120"
+       x2="100"
+       x1="50" />
+    <line
+       id="line706"
+       y2="169"
+       style="fill:none"
+       y1="120"
+       x2="100"
+       x1="100" />
+    <polygon
+       id="polygon708"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon710"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g712"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(475.4118,364.5588)">
+    <line
+       id="line714"
+       y2="100"
+       style="fill:none"
+       y1="50"
+       x2="190"
+       x1="190" />
+    <line
+       id="line716"
+       y2="100"
+       style="fill:none"
+       y1="100"
+       x2="50"
+       x1="190" />
+    <line
+       id="line718"
+       y2="169"
+       style="fill:none"
+       y1="100"
+       x2="50"
+       x1="50" />
+    <polygon
+       id="polygon720"
+       points="190,50 184,62 196,62 190,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon722"
+       points="190,50 184,62 196,62 190,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g724"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(655.4118,364.5588)">
+    <line
+       id="line726"
+       y2="129"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="50" />
+    <polygon
+       id="polygon728"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon730"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g732"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(545.4118,364.5588)">
+    <line
+       id="line734"
+       y2="120"
+       style="fill:none"
+       y1="50"
+       x2="140"
+       x1="140" />
+    <line
+       id="line736"
+       y2="120"
+       style="fill:none"
+       y1="120"
+       x2="50"
+       x1="140" />
+    <line
+       id="line738"
+       y2="159"
+       style="fill:none"
+       y1="120"
+       x2="50"
+       x1="50" />
+    <polygon
+       id="polygon740"
+       points="140,50 134,62 146,62 140,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon742"
+       points="140,50 134,62 146,62 140,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g744"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(65.41176,504.5588)">
+    <line
+       id="line746"
+       y2="130"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="50" />
+    <line
+       id="line748"
+       y2="130"
+       style="fill:none"
+       y1="130"
+       x2="359"
+       x1="50" />
+    <polygon
+       id="polygon750"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon752"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g754"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(745.4118,364.5588)">
+    <line
+       id="line756"
+       y2="50"
+       style="fill:none"
+       y1="169"
+       x2="50"
+       x1="50" />
+    <line
+       id="line758"
+       y2="155"
+       style="fill:none"
+       y1="164"
+       x2="44"
+       x1="56" />
+    <line
+       id="line760"
+       y2="155"
+       style="fill:none"
+       y1="164"
+       x2="56"
+       x1="44" />
+    <line
+       id="line762"
+       y2="62"
+       style="fill:none"
+       y1="50"
+       x2="44"
+       x1="50" />
+    <line
+       id="line764"
+       y2="62"
+       style="fill:none"
+       y1="50"
+       x2="56"
+       x1="50" />
+  </g>
+  <g
+     id="g766"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(795.4118,344.5588)">
+    <line
+       id="line768"
+       y2="50"
+       style="fill:none"
+       y1="189"
+       x2="90"
+       x1="90" />
+    <line
+       id="line770"
+       y2="50"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="90" />
+    <line
+       id="line772"
+       y2="175"
+       style="fill:none"
+       y1="184"
+       x2="84"
+       x1="96" />
+    <line
+       id="line774"
+       y2="175"
+       style="fill:none"
+       y1="184"
+       x2="96"
+       x1="84" />
+    <line
+       id="line776"
+       y2="56"
+       style="fill:none"
+       y1="50"
+       x2="62"
+       x1="50" />
+    <line
+       id="line778"
+       y2="44"
+       style="fill:none"
+       y1="50"
+       x2="62"
+       x1="50" />
+  </g>
+  <g
+     id="g780"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(405.4118,334.5588)">
+    <line
+       id="line782"
+       y2="50"
+       style="fill:none"
+       y1="199"
+       x2="50"
+       x1="50" />
+    <line
+       id="line784"
+       y2="50"
+       style="fill:none"
+       y1="50"
+       x2="209"
+       x1="50" />
+    <line
+       id="line786"
+       y2="185"
+       style="fill:none"
+       y1="194"
+       x2="44"
+       x1="56" />
+    <line
+       id="line788"
+       y2="185"
+       style="fill:none"
+       y1="194"
+       x2="56"
+       x1="44" />
+    <line
+       id="line790"
+       y2="44"
+       style="fill:none"
+       y1="50"
+       x2="197"
+       x1="209" />
+    <line
+       id="line792"
+       y2="56"
+       style="fill:none"
+       y1="50"
+       x2="197"
+       x1="209" />
+  </g>
+  <g
+     id="g794"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(435.4118,344.5588)">
+    <line
+       id="line796"
+       y2="50"
+       style="fill:none"
+       y1="189"
+       x2="50"
+       x1="50" />
+    <line
+       id="line798"
+       y2="50"
+       style="fill:none"
+       y1="50"
+       x2="179"
+       x1="50" />
+    <line
+       id="line800"
+       y2="175"
+       style="fill:none"
+       y1="184"
+       x2="44"
+       x1="56" />
+    <line
+       id="line802"
+       y2="175"
+       style="fill:none"
+       y1="184"
+       x2="56"
+       x1="44" />
+    <line
+       id="line804"
+       y2="44"
+       style="fill:none"
+       y1="50"
+       x2="167"
+       x1="179" />
+    <line
+       id="line806"
+       y2="56"
+       style="fill:none"
+       y1="50"
+       x2="167"
+       x1="179" />
+  </g>
+  <g
+     id="g808"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(45.41176,4.558783)">
+    <line
+       id="line810"
+       y2="150"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="50" />
+    <line
+       id="line812"
+       y2="150"
+       style="fill:none"
+       y1="150"
+       x2="109"
+       x1="50" />
+    <polygon
+       id="polygon814"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon816"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g818"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(65.41176,4.558783)">
+    <line
+       id="line820"
+       y2="80"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="50" />
+    <line
+       id="line822"
+       y2="80"
+       style="fill:none"
+       y1="80"
+       x2="89"
+       x1="50" />
+    <polygon
+       id="polygon824"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon826"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g828"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(315.4118,94.55878)">
+    <line
+       id="line830"
+       y2="50"
+       style="fill:none"
+       y1="50"
+       x2="179"
+       x1="50" />
+    <line
+       id="line832"
+       y2="44"
+       style="fill:none"
+       y1="56"
+       x2="63"
+       x1="54" />
+    <line
+       id="line834"
+       y2="56"
+       style="fill:none"
+       y1="44"
+       x2="63"
+       x1="54" />
+    <line
+       id="line836"
+       y2="44"
+       style="fill:none"
+       y1="50"
+       x2="167"
+       x1="179" />
+    <line
+       id="line838"
+       y2="56"
+       style="fill:none"
+       y1="50"
+       x2="167"
+       x1="179" />
+  </g>
+  <g
+     id="g840"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(315.4118,154.5588)">
+    <line
+       id="line842"
+       y2="50"
+       style="fill:none"
+       y1="50"
+       x2="109"
+       x1="50" />
+  </g>
+  <g
+     id="g844"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(275.4118,24.55878)">
+    <line
+       id="line846"
+       y2="50"
+       style="fill:none"
+       y1="50"
+       x2="89"
+       x1="50" />
+    <line
+       id="line848"
+       y2="44"
+       style="fill:none"
+       y1="56"
+       x2="63"
+       x1="54" />
+    <line
+       id="line850"
+       y2="56"
+       style="fill:none"
+       y1="44"
+       x2="63"
+       x1="54" />
+    <line
+       id="line852"
+       y2="44"
+       style="fill:none"
+       y1="50"
+       x2="77"
+       x1="89" />
+    <line
+       id="line854"
+       y2="56"
+       style="fill:none"
+       y1="50"
+       x2="77"
+       x1="89" />
+  </g>
+  <g
+     id="g856"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(25.41176,4.558783)">
+    <line
+       id="line858"
+       y2="359"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="50" />
+    <polygon
+       id="polygon860"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon862"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:none" />
+  </g>
+</svg>
diff --git a/doc/internal/wraptypes.rst b/doc/internal/wraptypes.rst
new file mode 100644
index 0000000..78bcf86
--- /dev/null
+++ b/doc/internal/wraptypes.rst
@@ -0,0 +1,297 @@
+wraptypes
+=========
+
+wraptypes is a general utility for creating ctypes wrappers from C header
+files.  The front-end is ``tools/wraptypes/wrap.py``, for usage::
+
+    python tools/wraptypes/wrap.py -h
+
+There are three components to wraptypes:
+
+preprocessor.py
+    Interprets preprocessor declarations and converts the source header files
+    into a list of tokens.
+cparser.py
+    Parses the preprocessed tokens for type and function declarations and
+    calls ``handle_`` methods on the class CParser in a similar manner to a
+    SAX parser.
+ctypesparser.py
+    Interprets C declarations and types from CParser and creates corresponding
+    ctypes declarations, calling ``handle_`` methods on the class
+    CtypesParser.
+
+.. image: wraptypes-class.svg
+
+The front-end ``wrap.py`` provides a simple subclass of ``CtypesParser``,
+``CtypesWrapper``, which writes the ctypes declarations found to a file in a
+format that can be imported as a module.
+
+Parser Modifications
+--------------------
+
+The parsers are built upon a modified version of `PLY`_, a Python
+implementation of lex and yacc. The modified source is included in
+the ``wraptypes`` directory.  The modifications are:
+
+* Grammar is abstracted out of Parser, so multiple grammars can easily be
+  defined in the same module.
+* Tokens and symbols keep track of their filename as well as line number.
+* Lexer state can be pushed onto a stack.
+
+The first time the parsers are run (or after they are modified), PLY creates
+``pptab.py`` and ``parsetab.py`` in the current directory.  These are
+the generated state machines, which can take a few seconds to generate.
+The file ``parser.out`` is created if debugging is enabled, and contains the
+parser description (of the last parser that was generated), which is essential
+for debugging.
+
+.. _PLY: http://www.dabeaz.com/ply/
+
+Preprocessor
+------------
+
+The grammar and parser are defined in ``preprocessor.py``.
+
+There is only one lexer state.  Each token has a type which is a string (e.g.
+``'CHARACTER_CONSTANT'``) and a value.  Token values, when read directly from
+the source file are only ever strings.  When tokens are written to the output
+list they sometimes have tuple values (for example, a ``PP_DEFINE`` token on
+output).
+
+Two lexer classes are defined: ``PreprocessorLexer``, which reads a stack of
+files (actually strings) as input, and ``TokenListLexer``, which reads from a
+list of already-parsed tokens (used for parsing expressions).
+
+The preprocessing entry-point is the ``PreprocessorParser`` class.  This
+creates a ``PreprocessorLexer`` and its grammar during construction.  The
+system include path includes the GCC search path by default but can be
+modified by altering the ``include_path`` and ``framework_path`` lists.  The
+``system_headers`` dict allows header files to be implied on the search path
+that don't exist.  For example, by setting::
+
+    system_headers['stdlib.h'] = '''#ifndef STDLIB_H
+    #define STDLIB_H
+
+    /* ... */
+    #endif
+    '''
+
+you can insert your own custom header in place of the one on the filesystem.
+This is useful when parsing headers from network locations.
+
+Parsing begins when ``parse`` is called.  Specify one or both of a filename
+and a string of data.  If ``debug`` kwarg is True, syntax errors dump the
+parser state instead of just the line number where they occurred.
+
+The production rules specify the actions; these are implemented in
+``PreprocessorGrammar``.  The actions call methods on ``PreprocessorParser``,
+such as:
+
+* ``include(self, header)``, to push another file onto the lexer.
+* ``include_system(self, header)``, to search the system path for a file to
+  push onto the lexer
+* ``error(self, message, filename, line)``, to signal a parse error.  Not
+  all syntax errors get this far, due to limitations in the parser.  A parse
+  error at EOF will just print to stderr.
+* ``write(self, tokens)``, to write tokens to the output list.  This is
+  the default action when no preprocessing declaratives are being parsed.
+
+The parser has a stack of ``ExecutionState``, which specifies whether the
+current tokens being parsed are ignored or not (tokens are ignored in an
+``#if`` that evaluates to 0).  This is a little more complicated than just a
+boolean flag:  the parser must also ignore #elif conditions that can have no
+effect.  The ``enable_declaratives`` and ``enable_elif_conditionals`` return
+True if the top-most ``ExecutionState`` allows declaratives and ``#elif``
+conditionals to be parsed, respecitively.  The execution state stack is
+modified with the ``condition_*`` methods.
+
+``PreprocessorParser`` has a ``PreprocessorNamespace`` which keeps track of
+the currently defined macros.  You can create and specify your own namespace,
+or use one that is created by default.  The default namespace includes GCC
+platform macros needed for parsing system headers, and some of the STDC
+macros.
+
+Macros are expanded when tokens are written to the output list, and when
+conditional expressions are parsed.
+``PreprocessorNamespace.apply_macros(tokens)`` takes care of this, replacing
+function parameters, variable arguments, macro objects and (mostly) avoiding
+infinite recursion.  It does not yet handle the ``#`` and ``##`` operators,
+which are needed to parse the Windows system headers.
+
+The process for evaluating a conditional (``#if`` or ``#elif``) is:
+
+1. Tokens between ``PP_IF`` or ``PP_ELIF`` and ``NEWLINE`` are expanded
+   by ``apply_macros``.
+2. The resulting list of tokens is used to construct a ``TokenListLexer``.
+3. This lexer is used as input to a ``ConstantExpressionParser``.  This parser
+   uses the ``ConstantExpressionGrammar``, which builds up an AST of
+   ``ExpressionNode`` objects.
+4. ``parse`` is called on the ``ConstantExpressionParser``, which returns the
+   resulting top-level ``ExpressionNode``, or ``None`` if there was a syntax
+   error.
+5. The ``evaluate`` method of the ``ExpressionNode`` is called with the
+   preprocessor's namespace as the evaluation context.  This allows the
+   expression nodes to resolve ``defined`` operators.
+6. The result of ``evaluate`` is always an int; non-zero values are treated as
+   True.
+
+Because pyglet requires special knowledge of the preprocessor declaratives
+that were encountered in the source, these are encoded as pseudo-tokens within
+the output token list.  For example, after a ``#ifndef`` is evaluated, it
+is written to the token list as a ``PP_IFNDEF`` token.
+
+``#define`` is handled specially.  After applying it to the namespace, it is
+parsed as an expression immediately.  This is allowed (and often expected) to
+fail.  If it does not fail, a ``PP_DEFINE_CONSTANT`` token is created, and the
+value is the result of evaluatin the expression.  Otherwise, a ``PP_DEFINE``
+token is created, and the value is the string concatenation of the tokens
+defined.  Special handling of parseable expressions makes it simple to later
+parse constants defined as, for example::
+
+    #define RED_SHIFT 8
+    #define RED_MASK (0x0f << RED_SHIFT)
+
+The preprocessor can be tested/debugged by running ``preprocessor.py``
+stand-alone with a header file as the sole argument.  The resulting token list
+will be written to stdout.
+
+CParser
+-------
+
+The lexer for ``CParser``, ``CLexer``, takes as input a list of tokens output
+from the preprocessor.  The special preprocessor tokens such as ``PP_DEFINE``
+are intercepted here and handled immediately; hence they can appear anywhere
+in the source header file without causing problems with the parser.  At this
+point ``IDENTIFIER`` tokens which are found to be the name of a defined type
+(the set of defined types is updated continuously during parsing) are
+converted to ``TYPE_NAME`` tokens.
+
+The entry-point to parsing C source is the ``CParser`` class.  This creates a
+preprocessor in its constructor, and defines some default types such as
+``wchar_t`` and ``__int64_t``.  These can be disabled with kwargs.
+
+Preprocessing can be quite time-consuming, especially on OS X where thousands
+of ``#include`` declaratives are processed when Carbon is parsed.  To minimise
+the time required to parse similar (or the same, while debugging) header
+files, the token list from preprocessing is cached and reused where possible.
+
+This is handled by ``CPreprocessorParser``, which overrides ``push_file`` to
+check with ``CParser`` if the desired file is cached.  The cache is checked
+against the file's modification timestamp as well as a "memento" that
+describes the currently defined tokens.  This is intended to avoid using a
+cached file that would otherwise be parsed differently due to the defined
+macros.  It is by no means perfect; for example, it won't pick up on a macro
+that has been defined differently.  It seems to work well enough for the
+header files pyglet requires.
+
+The header cache is saved and loaded automatically in the working directory
+as ``.header.cache``.  The cache should be deleted if you make changes to the
+preprocessor, or are experiencing cache errors (these are usually accompanied
+by a "what-the?" exclamation from the user).
+
+The actions in the grammar construct parts of a "C object model" and call
+methods on ``CParser``.  The C object model is not at all complete, containing
+only what pyglet (and any other ctypes-wrapping application) requires.  The
+classes in the object model are:
+
+Declaration
+    A single declaration occuring outside of a function body.  This includes
+    type declarations, function declarations and variable declarations.  The
+    attributes are ``declarator`` (see below), ``type`` (a Type object) and
+    ``storage`` (for example, 'typedef', 'const', 'static', 'extern', etc).
+Declarator
+    A declarator is a thing being declared.  Declarators have an
+    ``identifier`` (the name of it, None if the declarator is abstract, as in
+    some function parameter declarations), an optional ``initializer``
+    (currently ignored), an optional linked-list of ``array`` (giving the
+    dimensions of the array) and an optional list of ``parameters`` (if the
+    declarator is a function).
+Pointer
+    This is a type of declarator that is dereferenced via ``pointer`` to
+    another declarator.
+Array
+    Array has size (an int, its dimension, or None if unsized) and a pointer
+    ``array`` to the next array dimension, if any.
+Parameter
+    A function parameter consisting of a ``type`` (Type object), ``storage``
+    and ``declarator``.
+Type
+    Type has a list of ``qualifiers`` (e.g. 'const', 'volatile', etc) and
+    ``specifiers`` (the meaty bit).
+TypeSpecifier
+    A base TypeSpecifier is just a string, such as ``'int'`` or ``'Foo'`` or
+    ``'unsigned'``.  Note that types can have multiple TypeSpecifiers; not
+    all combinations are valid.
+StructTypeSpecifier
+    This is the specifier for a struct or union (if ``is_union`` is True)
+    type.  ``tag`` gives the optional ``foo`` in ``struct foo`` and
+    ``declarations`` is the meat (an empty list for an opaque or unspecified
+    struct).
+EnumSpecifier
+    This is the specifier for an enum type.  ``tag`` gives the optional
+    ``foo`` in ``enum foo`` and ``enumerators`` is the list of Enumerator
+    objects (an empty list for an unspecified enum).
+Enumerator
+    Enumerators exist only within EnumSpecifier.  Contains ``name`` and
+    ``expression``, an ExpressionNode object.
+
+The ``ExpressionNode`` object hierarchy is similar to that used in the
+preprocessor, but more fully-featured, and using a different
+``EvaluationContext`` which can evaluate identifiers and the ``sizeof``
+operator (currently it actually just returns 0 for both).
+
+Methods are called on CParser as declarations and preprocessor declaratives
+are parsed.  The are mostly self explanatory.  For example:
+
+handle_ifndef(self, name, filename, lineno)
+    An ``#ifndef`` was encountered testing the macro ``name`` in file
+    ``filename`` at line ``lineno``.
+handle_declaration(self, declaration, filename, lineno)
+    ``declaration`` is an instance of Declaration.
+
+These methods should be overridden by a subclass to provide functionality.
+The ``DebugCParser`` does this and prints out the arguments to each
+``handle_`` method.
+
+The ``CParser`` can be tested in isolation by running it stand-alone with the
+filename of a header as the sole argument.  A ``DebugCParser`` will be
+constructed and used to parse the header.
+
+CtypesParser
+------------
+
+``CtypesParser`` is implemented in ``ctypesparser.py``.  It is a subclass of
+``CParser`` and implements the ``handle_`` methods to provide a more
+ctypes-friendly interpretation of the declarations.
+
+To use, subclass and override the methods:
+
+handle_ctypes_constant(self, name, value, filename, lineno)
+    An integer or float constant (in a ``#define``).
+handle_ctypes_type_definition(self, name, ctype, filename, lineno)
+    A ``typedef`` declaration.  See below for type of ``ctype``.
+handle_ctypes_function(self, name, restype, argtypes, filename, lineno)
+    A function declaration with the given return type and argument list.
+handle_ctypes_variable(self, name, ctype, filename, lineno)
+    Any other non-``static`` declaration.
+
+Types are represented by instances of ``CtypesType``.  This is more easily
+manipulated than a "real" ctypes type.  There are subclasses for
+``CtypesPointer``, ``CtypesArray``, ``CtypesFunction``, and so on; see the
+module for details.
+
+Each ``CtypesType`` class implements the ``visit`` method, which can be used,
+Visitor pattern style, to traverse the type hierarchy.  Call the ``visit``
+method of any type with an implementation of ``CtypesTypeVisitor``: all
+pointers, array bases, function parameters and return types are traversed
+automatically (struct members are not, however).
+
+This is useful when writing the contents of a struct or enum.  Before writing
+a type declaration for a struct type (which would consist only of the struct's
+tag), ``visit`` the type and handle the ``visit_struct`` method on the visitor
+to print out the struct's members first.  Similarly for enums.
+
+``ctypesparser.py`` can not be run stand-alone.  ``wrap.py`` provides a
+straight-forward implementation that writes a module of ctypes wrappers.  It
+can filter the output based on the originating filename.  See the module
+docstring for usage and extension details.
diff --git a/doc/make.bat b/doc/make.bat
new file mode 100644
index 0000000..6a67042
--- /dev/null
+++ b/doc/make.bat
@@ -0,0 +1,192 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+	set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+	:help
+	echo.Please use `make ^<target^>` where ^<target^> is one of
+	echo.  html       to make standalone HTML files
+	echo.  dirhtml    to make HTML files named index.html in directories
+	echo.  singlehtml to make a single large HTML file
+	echo.  pickle     to make pickle files
+	echo.  json       to make JSON files
+	echo.  htmlhelp   to make HTML files and a HTML help project
+	echo.  qthelp     to make HTML files and a qthelp project
+	echo.  devhelp    to make HTML files and a Devhelp project
+	echo.  epub       to make an epub
+	echo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+	echo.  text       to make text files
+	echo.  man        to make manual pages
+	echo.  texinfo    to make Texinfo files
+	echo.  gettext    to make PO message catalogs
+	echo.  changes    to make an overview over all changed/added/deprecated items
+	echo.  linkcheck  to check all external links for integrity
+	echo.  doctest    to run all doctests embedded in the documentation if enabled
+	goto end
+)
+
+if "%1" == "clean" (
+	for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+	del /q /s %BUILDDIR%\*
+    mkdir %~dp0\%BUILDDIR%\html
+    echo NUL > %~dp0\%BUILDDIR%\html\warnings.txt
+	goto end
+)
+
+if "%1" == "html" (
+	%SPHINXBUILD% -w %BUILDDIR%/html/warnings.txt -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+	goto end
+)
+
+if "%1" == "dirhtml" (
+	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+	goto end
+)
+
+if "%1" == "singlehtml" (
+	%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+	goto end
+)
+
+if "%1" == "pickle" (
+	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can process the pickle files.
+	goto end
+)
+
+if "%1" == "json" (
+	%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can process the JSON files.
+	goto end
+)
+
+if "%1" == "htmlhelp" (
+	%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+	goto end
+)
+
+if "%1" == "qthelp" (
+	%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+	echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Pyglet.qhcp
+	echo.To view the help file:
+	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Pyglet.ghc
+	goto end
+)
+
+if "%1" == "devhelp" (
+	%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished.
+	goto end
+)
+
+if "%1" == "epub" (
+	%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The epub file is in %BUILDDIR%/epub.
+	goto end
+)
+
+if "%1" == "latex" (
+	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+	goto end
+)
+
+if "%1" == "text" (
+	%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The text files are in %BUILDDIR%/text.
+	goto end
+)
+
+if "%1" == "man" (
+	%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The manual pages are in %BUILDDIR%/man.
+	goto end
+)
+
+if "%1" == "texinfo" (
+	%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+	goto end
+)
+
+if "%1" == "gettext" (
+	%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+	goto end
+)
+
+if "%1" == "changes" (
+	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.The overview file is in %BUILDDIR%/changes.
+	goto end
+)
+
+if "%1" == "linkcheck" (
+	%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+	goto end
+)
+
+if "%1" == "doctest" (
+	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+	goto end
+)
+
+:end
diff --git a/doc/modules/app.rst b/doc/modules/app.rst
new file mode 100644
index 0000000..5199061
--- /dev/null
+++ b/doc/modules/app.rst
@@ -0,0 +1,55 @@
+pyglet.app
+==========
+
+.. automodule:: pyglet.app
+
+Classes
+-------
+
+.. autoclass:: EventLoop
+
+  .. rubric:: Methods
+
+  .. automethod:: run
+  .. automethod:: exit
+  .. automethod:: sleep
+
+  .. rubric:: Events
+
+  .. automethod:: on_enter
+  .. automethod:: on_exit
+  .. automethod:: on_window_close
+
+  .. rubric:: Attributes
+
+  .. autoattribute:: has_exit
+
+  .. rubric:: Methods (internal)
+
+  .. automethod:: enter_blocking
+  .. automethod:: exit_blocking
+  .. automethod:: idle
+
+.. autoclass:: PlatformEventLoop
+  :members:
+  :undoc-members:
+
+Functions
+---------
+
+.. autofunction:: run
+.. autofunction:: exit
+
+Attributes
+----------
+
+.. autodata:: event_loop
+
+.. autodata:: platform_event_loop
+
+.. autodata:: windows
+
+Exceptions
+----------
+
+.. autoclass:: AppException
diff --git a/doc/modules/canvas.rst b/doc/modules/canvas.rst
new file mode 100644
index 0000000..5bd6c57
--- /dev/null
+++ b/doc/modules/canvas.rst
@@ -0,0 +1,18 @@
+pyglet.canvas
+=============
+
+.. automodule:: pyglet.canvas
+  :members:
+  :undoc-members:
+
+.. autoclass:: Display
+  :members:
+  :undoc-members:
+
+.. autoclass:: Screen
+  :members:
+  :undoc-members:
+
+.. autoclass:: Canvas
+  :members:
+  :undoc-members:
diff --git a/doc/modules/clock.rst b/doc/modules/clock.rst
new file mode 100644
index 0000000..b73b405
--- /dev/null
+++ b/doc/modules/clock.rst
@@ -0,0 +1,6 @@
+pyglet.clock
+============
+
+.. automodule:: pyglet.clock
+  :members:
+  :undoc-members:
diff --git a/doc/modules/event.rst b/doc/modules/event.rst
new file mode 100644
index 0000000..e69eff5
--- /dev/null
+++ b/doc/modules/event.rst
@@ -0,0 +1,6 @@
+pyglet.event
+============
+
+.. automodule:: pyglet.event
+  :members:
+  :undoc-members:
diff --git a/doc/modules/font.rst b/doc/modules/font.rst
new file mode 100644
index 0000000..f03aad1
--- /dev/null
+++ b/doc/modules/font.rst
@@ -0,0 +1,6 @@
+pyglet.font
+===========
+
+.. automodule:: pyglet.font
+  :members:
+  :undoc-members:
diff --git a/doc/modules/gl.rst b/doc/modules/gl.rst
new file mode 100644
index 0000000..9c49b77
--- /dev/null
+++ b/doc/modules/gl.rst
@@ -0,0 +1,27 @@
+pyglet.gl
+=========
+
+.. currentmodule:: pyglet.gl
+
+.. automodule:: pyglet.gl
+  :members:
+  :undoc-members:
+
+.. autoclass:: GLException
+
+.. autoclass:: ObjectSpace
+  :members:
+  :undoc-members:
+
+.. autoclass:: Config
+  :members:
+  :undoc-members:
+
+.. autoclass:: CanvasConfig
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: Context
+  :members:
+  :undoc-members:
diff --git a/doc/modules/graphics/allocation.rst b/doc/modules/graphics/allocation.rst
new file mode 100644
index 0000000..73fad24
--- /dev/null
+++ b/doc/modules/graphics/allocation.rst
@@ -0,0 +1,6 @@
+pyglet.graphics.allocation
+==========================
+
+.. automodule:: pyglet.graphics.allocation
+  :members:
+  :undoc-members:
diff --git a/doc/modules/graphics/index.rst b/doc/modules/graphics/index.rst
new file mode 100644
index 0000000..a40330a
--- /dev/null
+++ b/doc/modules/graphics/index.rst
@@ -0,0 +1,18 @@
+pyglet.graphics
+===============
+
+.. rubric:: Submodules
+
+.. toctree::
+   :maxdepth: 1
+
+   allocation
+   vertexattribute
+   vertexbuffer
+   vertexdomain
+
+.. rubric:: Details
+
+.. automodule:: pyglet.graphics
+  :members:
+  :undoc-members:
diff --git a/doc/modules/graphics/vertexattribute.rst b/doc/modules/graphics/vertexattribute.rst
new file mode 100644
index 0000000..1c197b5
--- /dev/null
+++ b/doc/modules/graphics/vertexattribute.rst
@@ -0,0 +1,6 @@
+pyglet.graphics.vertexattribute
+===============================
+
+.. automodule:: pyglet.graphics.vertexattribute
+  :members:
+  :undoc-members:
diff --git a/doc/modules/graphics/vertexbuffer.rst b/doc/modules/graphics/vertexbuffer.rst
new file mode 100644
index 0000000..dc26147
--- /dev/null
+++ b/doc/modules/graphics/vertexbuffer.rst
@@ -0,0 +1,6 @@
+pyglet.graphics.vertexbuffer
+============================
+
+.. automodule:: pyglet.graphics.vertexbuffer
+  :members:
+  :undoc-members:
diff --git a/doc/modules/graphics/vertexdomain.rst b/doc/modules/graphics/vertexdomain.rst
new file mode 100644
index 0000000..47ff45d
--- /dev/null
+++ b/doc/modules/graphics/vertexdomain.rst
@@ -0,0 +1,6 @@
+pyglet.graphics.vertexdomain
+============================
+
+.. automodule:: pyglet.graphics.vertexdomain
+  :members:
+  :undoc-members:
diff --git a/doc/modules/gui.rst b/doc/modules/gui.rst
new file mode 100644
index 0000000..b6872c5
--- /dev/null
+++ b/doc/modules/gui.rst
@@ -0,0 +1,39 @@
+pyglet.gui
+===========
+
+.. automodule:: pyglet.gui
+
+Classes
+-------
+
+.. autoclass:: WidgetBase
+
+  .. rubric:: Attributes
+
+  .. autoattribute:: x
+  .. autoattribute:: y
+  .. autoattribute:: width
+  .. autoattribute:: height
+  .. autoattribute:: aabb
+
+.. autoclass:: PushButton
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: ToggleButton
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: Slider
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: TextEntry
+  :members:
+  :undoc-members:
+  :show-inheritance:
+  
+  
\ No newline at end of file
diff --git a/doc/modules/image/animation.rst b/doc/modules/image/animation.rst
new file mode 100644
index 0000000..d71af5c
--- /dev/null
+++ b/doc/modules/image/animation.rst
@@ -0,0 +1,6 @@
+pyglet.image.animation
+======================
+
+.. automodule:: pyglet.image.animation
+  :members:
+  :undoc-members:
diff --git a/doc/modules/image/atlas.rst b/doc/modules/image/atlas.rst
new file mode 100644
index 0000000..3259870
--- /dev/null
+++ b/doc/modules/image/atlas.rst
@@ -0,0 +1,6 @@
+pyglet.image.atlas
+==================
+
+.. automodule:: pyglet.image.atlas
+  :members:
+  :undoc-members:
diff --git a/doc/modules/image/index.rst b/doc/modules/image/index.rst
new file mode 100644
index 0000000..5ff84af
--- /dev/null
+++ b/doc/modules/image/index.rst
@@ -0,0 +1,154 @@
+pyglet.image
+============
+
+.. rubric:: Submodules
+
+.. toctree::
+   :maxdepth: 1
+
+   atlas
+   animation
+
+.. rubric:: Details
+
+.. automodule:: pyglet.image
+
+Classes
+-------
+
+Images
+^^^^^^
+
+.. autoclass:: AbstractImage
+  :members:
+  :undoc-members:
+
+.. autoclass:: BufferImage
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: BufferImageMask
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: ColorBufferImage
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: DepthBufferImage
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: Texture
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: DepthTexture
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: TextureRegion
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: TileableTexture
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+Image Sequences
+^^^^^^^^^^^^^^^
+
+.. autoclass:: AbstractImageSequence
+  :members:
+  :undoc-members:
+
+.. autoclass:: TextureSequence
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: UniformTextureSequence
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: TextureGrid
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: Texture3D
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+Patterns
+^^^^^^^^
+
+.. autoclass:: ImagePattern
+  :members:
+  :undoc-members:
+
+.. autoclass:: CheckerImagePattern
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: SolidColorImagePattern
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+Data
+^^^^
+
+.. autoclass:: ImageData
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: CompressedImageData
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: ImageDataRegion
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+Other Classes
+^^^^^^^^^^^^^
+
+.. autoclass:: BufferManager
+  :members:
+  :undoc-members:
+
+.. autoclass:: ImageGrid
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+Functions
+---------
+
+.. autofunction:: create
+.. autofunction:: get_buffer_manager
+.. autofunction:: load
+.. autofunction:: load_animation
+.. autofunction:: get_max_texture_size
+
+Exceptions
+----------
+
+.. autoclass:: ImageException
+.. autoclass:: pyglet.image.codecs.ImageEncodeException
+.. autoclass:: pyglet.image.codecs.ImageDecodeException
diff --git a/doc/modules/info.rst b/doc/modules/info.rst
new file mode 100644
index 0000000..dd32a2f
--- /dev/null
+++ b/doc/modules/info.rst
@@ -0,0 +1,6 @@
+pyglet.info
+===========
+
+.. automodule:: pyglet.info
+  :members:
+  :undoc-members:
diff --git a/doc/modules/input.rst b/doc/modules/input.rst
new file mode 100644
index 0000000..2b6236c
--- /dev/null
+++ b/doc/modules/input.rst
@@ -0,0 +1,103 @@
+pyglet.input
+============
+
+.. automodule:: pyglet.input
+
+.. currentmodule:: pyglet.input
+
+Classes
+-------
+
+.. autoclass:: Device
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: Control
+  :show-inheritance:
+
+  .. rubric:: Events
+
+  .. automethod:: on_change
+
+  .. rubric:: Attributes
+
+  .. autoattribute:: value
+
+.. autoclass:: RelativeAxis
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: AbsoluteAxis
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: Button
+  :show-inheritance:
+
+  .. rubric:: Events
+
+  .. automethod:: on_press
+  .. automethod:: on_release
+
+  .. rubric:: Attributes
+
+  .. autoattribute:: value
+
+.. autoclass:: Joystick
+  :show-inheritance:
+
+  .. rubric:: Methods
+
+  .. automethod:: open
+  .. automethod:: close
+
+  .. rubric:: Events
+
+  .. automethod:: on_joyaxis_motion
+  .. automethod:: on_joyhat_motion
+  .. automethod:: on_joybutton_press
+  .. automethod:: on_joybutton_release
+
+.. autoclass:: AppleRemote
+  :show-inheritance:
+
+  .. rubric:: Methods
+
+  .. automethod:: open
+  .. automethod:: close
+
+  .. rubric:: Events
+
+  .. automethod:: on_button_press
+  .. automethod:: on_button_release
+
+.. autoclass:: Tablet
+  :undoc-members:
+
+Functions
+---------
+
+.. currentmodule:: pyglet.input
+
+.. autofunction:: get_apple_remote
+.. autofunction:: get_devices
+.. autofunction:: get_joysticks
+.. autofunction:: get_tablets
+
+Exceptions
+----------
+
+.. autoclass:: DeviceException
+  :members:
+  :undoc-members:
+
+.. autoclass:: DeviceOpenException
+  :members:
+  :undoc-members:
+
+.. autoclass:: DeviceExclusiveException
+  :members:
+  :undoc-members:
diff --git a/doc/modules/media.rst b/doc/modules/media.rst
new file mode 100644
index 0000000..e79cbc5
--- /dev/null
+++ b/doc/modules/media.rst
@@ -0,0 +1,107 @@
+pyglet.media
+============
+
+.. rubric:: Submodules
+
+.. toctree::
+   :maxdepth: 1
+
+   media_synthesis
+
+.. rubric:: Details
+
+.. currentmodule:: pyglet.media
+
+.. automodule:: pyglet.media
+
+Classes
+-------
+
+.. autoclass:: pyglet.media.player.Player
+  :members: loop
+
+  .. rubric:: Methods
+
+  .. automethod:: play
+  .. automethod:: pause
+  .. automethod:: queue
+  .. automethod:: seek
+  .. automethod:: seek_next_frame
+  .. automethod:: get_texture
+  .. automethod:: next_source
+  .. automethod:: delete
+  .. automethod:: update_texture
+
+  .. rubric:: Events
+
+  .. automethod:: on_eos
+  .. automethod:: on_player_eos
+  .. automethod:: on_player_next_source
+
+  .. rubric:: Attributes
+
+  .. autoattribute:: cone_inner_angle
+  .. autoattribute:: cone_outer_angle
+  .. autoattribute:: cone_orientation
+  .. autoattribute:: cone_outer_gain
+  .. autoattribute:: min_distance
+  .. autoattribute:: max_distance
+  .. autoattribute:: pitch
+  .. autoattribute:: playing
+  .. autoattribute:: position
+  .. autoattribute:: source
+  .. autoattribute:: texture
+  .. autoattribute:: time
+  .. autoattribute:: volume
+
+.. autoclass:: pyglet.media.player.PlayerGroup
+
+  .. automethod:: play
+  .. automethod:: pause
+
+.. autoclass:: pyglet.media.codecs.AudioFormat
+.. autoclass:: pyglet.media.codecs.VideoFormat
+.. autoclass:: pyglet.media.codecs.AudioData
+    :members:
+
+.. autoclass:: pyglet.media.codecs.SourceInfo
+    :members:
+
+.. autoclass:: Source
+  :members:
+
+.. autoclass:: StreamingSource
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: StaticSource
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: pyglet.media.codecs.StaticMemorySource
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: pyglet.media.drivers.listener.AbstractListener
+  :members:
+  :undoc-members:
+
+.. autoclass:: pyglet.media.events.MediaEvent
+    :members:
+
+Functions
+---------
+
+.. autofunction:: get_audio_driver
+.. autofunction:: load
+.. autofunction:: have_ffmpeg
+
+Exceptions
+----------
+
+.. automodule:: pyglet.media.exceptions
+  :members:
+  :undoc-members:
diff --git a/doc/modules/media_synthesis.rst b/doc/modules/media_synthesis.rst
new file mode 100644
index 0000000..bba2fd1
--- /dev/null
+++ b/doc/modules/media_synthesis.rst
@@ -0,0 +1,6 @@
+pyglet.media.synthesis
+======================
+
+.. automodule:: pyglet.media.synthesis
+  :members:
+  :undoc-members:
diff --git a/doc/modules/pyglet.rst b/doc/modules/pyglet.rst
new file mode 100644
index 0000000..fa7a767
--- /dev/null
+++ b/doc/modules/pyglet.rst
@@ -0,0 +1,6 @@
+pyglet
+======
+
+.. automodule:: pyglet
+  :members:
+  :noindex:
diff --git a/doc/modules/resource.rst b/doc/modules/resource.rst
new file mode 100644
index 0000000..5f7d84e
--- /dev/null
+++ b/doc/modules/resource.rst
@@ -0,0 +1,22 @@
+pyglet.resource
+===============
+
+.. automodule:: pyglet.resource
+  :members:
+  :undoc-members:
+
+.. autofunction:: reindex
+.. autofunction:: file
+.. autofunction:: location
+.. autofunction:: add_font
+.. autofunction:: image
+.. autofunction:: animation
+.. autofunction:: get_cached_image_names
+.. autofunction:: get_cached_animation_names
+.. autofunction:: get_texture_bins
+.. autofunction:: media
+.. autofunction:: texture
+.. autofunction:: html
+.. autofunction:: attributed
+.. autofunction:: text
+.. autofunction:: get_cached_texture_names
diff --git a/doc/modules/shapes.rst b/doc/modules/shapes.rst
new file mode 100644
index 0000000..7c54c68
--- /dev/null
+++ b/doc/modules/shapes.rst
@@ -0,0 +1,6 @@
+pyglet.shapes
+=============
+
+.. automodule:: pyglet.shapes
+    :members:
+    :inherited-members:
diff --git a/doc/modules/sprite.rst b/doc/modules/sprite.rst
new file mode 100644
index 0000000..df31f85
--- /dev/null
+++ b/doc/modules/sprite.rst
@@ -0,0 +1,38 @@
+pyglet.sprite
+=============
+
+.. automodule:: pyglet.sprite
+
+.. autoclass:: Sprite
+
+  .. rubric:: Methods
+
+  .. automethod:: delete
+  .. automethod:: draw
+  .. automethod:: update
+
+  .. rubric:: Events
+
+  .. automethod:: on_animation_end
+
+  .. rubric:: Attributes
+
+  .. autoattribute:: batch
+  .. autoattribute:: color
+  .. autoattribute:: group
+  .. autoattribute:: height
+  .. autoattribute:: image
+  .. autoattribute:: opacity
+  .. autoattribute:: position
+  .. autoattribute:: rotation
+  .. autoattribute:: scale
+  .. autoattribute:: scale_x
+  .. autoattribute:: scale_y
+  .. autoattribute:: visible
+  .. autoattribute:: width
+  .. autoattribute:: x
+  .. autoattribute:: y
+
+.. autoclass:: SpriteGroup
+  :members:
+  :undoc-members:
diff --git a/doc/modules/text/caret.rst b/doc/modules/text/caret.rst
new file mode 100644
index 0000000..60891c4
--- /dev/null
+++ b/doc/modules/text/caret.rst
@@ -0,0 +1,6 @@
+pyglet.text.caret
+=================
+
+.. automodule:: pyglet.text.caret
+  :members:
+  :undoc-members:
diff --git a/doc/modules/text/document.rst b/doc/modules/text/document.rst
new file mode 100644
index 0000000..d4405f8
--- /dev/null
+++ b/doc/modules/text/document.rst
@@ -0,0 +1,6 @@
+pyglet.text.document
+====================
+
+.. automodule:: pyglet.text.document
+  :members:
+  :undoc-members:
diff --git a/doc/modules/text/index.rst b/doc/modules/text/index.rst
new file mode 100644
index 0000000..b366fd6
--- /dev/null
+++ b/doc/modules/text/index.rst
@@ -0,0 +1,17 @@
+pyglet.text
+===========
+
+.. rubric:: Submodules
+
+.. toctree::
+   :maxdepth: 1
+
+   caret
+   document
+   layout
+
+.. rubric:: Details
+
+.. automodule:: pyglet.text
+  :members:
+  :undoc-members:
diff --git a/doc/modules/text/layout.rst b/doc/modules/text/layout.rst
new file mode 100644
index 0000000..759cc7c
--- /dev/null
+++ b/doc/modules/text/layout.rst
@@ -0,0 +1,6 @@
+pyglet.text.layout
+==================
+
+.. automodule:: pyglet.text.layout
+  :members:
+  :undoc-members:
diff --git a/doc/modules/window.rst b/doc/modules/window.rst
new file mode 100644
index 0000000..e0f071a
--- /dev/null
+++ b/doc/modules/window.rst
@@ -0,0 +1,148 @@
+pyglet.window
+=============
+
+.. rubric:: Submodules
+
+.. toctree::
+   :maxdepth: 1
+
+   window_key
+   window_mouse
+
+.. rubric:: Details
+
+.. automodule:: pyglet.window
+
+Classes
+-------
+
+.. autoclass:: Window
+  :show-inheritance:
+
+  .. rubric:: Methods
+
+  .. automethod:: activate
+  .. automethod:: clear
+  .. automethod:: close
+  .. automethod:: dispatch_event
+  .. automethod:: dispatch_events
+  .. automethod:: draw_mouse_cursor
+  .. automethod:: flip
+  .. automethod:: get_location
+  .. automethod:: get_size
+  .. automethod:: get_system_mouse_cursor
+  .. automethod:: maximize
+  .. automethod:: minimize
+  .. automethod:: set_caption
+  .. automethod:: set_exclusive_keyboard
+  .. automethod:: set_exclusive_mouse
+  .. automethod:: set_fullscreen
+  .. automethod:: set_icon
+  .. automethod:: set_location
+  .. automethod:: set_maximum_size
+  .. automethod:: set_minimum_size
+  .. automethod:: set_mouse_cursor
+  .. automethod:: set_mouse_platform_visible
+  .. automethod:: set_mouse_visible
+  .. automethod:: set_size
+  .. automethod:: set_visible
+  .. automethod:: switch_to
+
+  .. rubric:: Events
+
+  .. automethod:: on_activate
+  .. automethod:: on_close
+  .. automethod:: on_context_lost
+  .. automethod:: on_context_state_lost
+  .. automethod:: on_deactivate
+  .. automethod:: on_draw
+  .. automethod:: on_expose
+  .. automethod:: on_hide
+  .. automethod:: on_key_press
+  .. automethod:: on_key_release
+  .. automethod:: on_mouse_drag
+  .. automethod:: on_mouse_enter
+  .. automethod:: on_mouse_leave
+  .. automethod:: on_mouse_motion
+  .. automethod:: on_mouse_press
+  .. automethod:: on_mouse_release
+  .. automethod:: on_mouse_scroll
+  .. automethod:: on_move
+  .. automethod:: on_resize
+  .. automethod:: on_show
+  .. automethod:: on_text
+  .. automethod:: on_text_motion
+  .. automethod:: on_text_motion_select
+
+  .. rubric:: Attributes
+
+  .. autoattribute:: caption
+  .. autoattribute:: config
+  .. autoattribute:: context
+  .. autoattribute:: display
+  .. autoattribute:: fullscreen
+  .. autoattribute:: has_exit
+  .. autoattribute:: height
+  .. autoattribute:: invalid
+  .. autoattribute:: resizeable
+  .. autoattribute:: screen
+  .. autoattribute:: style
+  .. autoattribute:: visible
+  .. autoattribute:: vsync
+  .. autoattribute:: width
+
+  .. rubric:: Class attributes: cursor names
+
+  .. autoattribute:: CURSOR_CROSSHAIR
+  .. autoattribute:: CURSOR_DEFAULT
+  .. autoattribute:: CURSOR_HAND
+  .. autoattribute:: CURSOR_HELP
+  .. autoattribute:: CURSOR_NO
+  .. autoattribute:: CURSOR_SIZE
+  .. autoattribute:: CURSOR_SIZE_DOWN
+  .. autoattribute:: CURSOR_SIZE_DOWN_LEFT
+  .. autoattribute:: CURSOR_SIZE_DOWN_RIGHT
+  .. autoattribute:: CURSOR_SIZE_LEFT
+  .. autoattribute:: CURSOR_SIZE_LEFT_RIGHT
+  .. autoattribute:: CURSOR_SIZE_RIGHT
+  .. autoattribute:: CURSOR_SIZE_UP
+  .. autoattribute:: CURSOR_SIZE_UP_DOWN
+  .. autoattribute:: CURSOR_SIZE_UP_LEFT
+  .. autoattribute:: CURSOR_SIZE_UP_RIGHT
+  .. autoattribute:: CURSOR_TEXT
+  .. autoattribute:: CURSOR_WAIT
+  .. autoattribute:: CURSOR_WAIT_ARROW
+
+  .. rubric:: Class attributes: window styles
+
+  .. autoattribute:: WINDOW_STYLE_BORDERLESS
+  .. autoattribute:: WINDOW_STYLE_DEFAULT
+  .. autoattribute:: WINDOW_STYLE_DIALOG
+  .. autoattribute:: WINDOW_STYLE_TOOL
+
+
+.. autoclass:: FPSDisplay
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+.. autoclass:: MouseCursor
+  :members:
+  :undoc-members:
+
+.. autoclass:: DefaultMouseCursor
+  :show-inheritance:
+
+.. autoclass:: ImageMouseCursor
+  :members:
+  :undoc-members:
+  :show-inheritance:
+
+
+Exceptions
+----------
+
+.. autoclass:: MouseCursorException
+.. autoclass:: NoSuchConfigException
+.. autoclass:: NoSuchDisplayException
+.. autoclass:: WindowException
diff --git a/doc/modules/window_key.rst b/doc/modules/window_key.rst
new file mode 100644
index 0000000..46394f8
--- /dev/null
+++ b/doc/modules/window_key.rst
@@ -0,0 +1,473 @@
+pyglet.window.key
+=================
+
+.. automodule:: pyglet.window.key
+  :members: KeyStateHandler, modifiers_string, symbol_string, motion_string,
+            user_key
+
+Key Constants
+-------------
+
+Modifier mask constants
+^^^^^^^^^^^^^^^^^^^^^^^
+
+.. list-table::
+
+  *
+    * ``MOD_SHIFT``
+  *
+    * ``MOD_CTRL``
+  *
+    * ``MOD_ALT``
+  *
+    * ``MOD_CAPSLOCK``
+  *
+    * ``MOD_NUMLOCK``
+  *
+    * ``MOD_WINDOWS``
+  *
+    * ``MOD_COMMAND``
+  *
+    * ``MOD_OPTION``
+  *
+    * ``MOD_SCROLLLOCK``
+  *
+    * ``MOD_FUNCTION``
+  *
+    * ``MOD_ACCEL`` (``MOD_CTRL`` on Windows & Linux, ``MOD_CMD`` on OS X)
+
+ASCII commands
+^^^^^^^^^^^^^^
+
+.. list-table::
+
+  *
+    * ``BACKSPACE``
+  *
+    * ``TAB``
+  *
+    * ``LINEFEED``
+  *
+    * ``CLEAR``
+  *
+    * ``RETURN``
+  *
+    * ``ENTER``
+  *
+    * ``PAUSE``
+  *
+    * ``SCROLLLOCK``
+  *
+    * ``SYSREQ``
+  *
+    * ``ESCAPE``
+  *
+    * ``SPACE``
+
+Cursor control and motion
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. list-table::
+
+  *
+    * ``HOME``
+  *
+    * ``LEFT``
+  *
+    * ``UP``
+  *
+    * ``RIGHT``
+  *
+    * ``DOWN``
+  *
+    * ``PAGEUP``
+  *
+    * ``PAGEDOWN``
+  *
+    * ``END``
+  *
+    * ``BEGIN``
+
+Misc functions
+^^^^^^^^^^^^^^
+
+.. list-table::
+
+  *
+    * ``DELETE``
+  *
+    * ``SELECT``
+  *
+    * ``PRINT``
+  *
+    * ``EXECUTE``
+  *
+    * ``INSERT``
+  *
+    * ``UNDO``
+  *
+    * ``REDO``
+  *
+    * ``MENU``
+  *
+    * ``FIND``
+  *
+    * ``CANCEL``
+  *
+    * ``HELP``
+  *
+    * ``BREAK``
+  *
+    * ``MODESWITCH``
+  *
+    * ``SCRIPTSWITCH``
+  *
+    * ``FUNCTION``
+
+Text motion constants
+^^^^^^^^^^^^^^^^^^^^^
+
+These are allowed to clash with key constants.
+
+.. list-table::
+
+  *
+    * ``MOTION_UP``
+  *
+    * ``MOTION_RIGHT``
+  *
+    * ``MOTION_DOWN``
+  *
+    * ``MOTION_LEFT``
+  *
+    * ``MOTION_NEXT_WORD``
+  *
+    * ``MOTION_PREVIOUS_WORD``
+  *
+    * ``MOTION_BEGINNING_OF_LINE``
+  *
+    * ``MOTION_END_OF_LINE``
+  *
+    * ``MOTION_NEXT_PAGE``
+  *
+    * ``MOTION_PREVIOUS_PAGE``
+  *
+    * ``MOTION_BEGINNING_OF_FILE``
+  *
+    * ``MOTION_END_OF_FILE``
+  *
+    * ``MOTION_BACKSPACE``
+  *
+    * ``MOTION_DELETE``
+
+Number pad
+^^^^^^^^^^
+
+.. list-table::
+
+  *
+    * ``NUMLOCK``
+  *
+    * ``NUM_SPACE``
+  *
+    * ``NUM_TAB``
+  *
+    * ``NUM_ENTER``
+  *
+    * ``NUM_F1``
+  *
+    * ``NUM_F2``
+  *
+    * ``NUM_F3``
+  *
+    * ``NUM_F4``
+  *
+    * ``NUM_HOME``
+  *
+    * ``NUM_LEFT``
+  *
+    * ``NUM_UP``
+  *
+    * ``NUM_RIGHT``
+  *
+    * ``NUM_DOWN``
+  *
+    * ``NUM_PRIOR``
+  *
+    * ``NUM_PAGE_UP``
+  *
+    * ``NUM_NEXT``
+  *
+    * ``NUM_PAGE_DOWN``
+  *
+    * ``NUM_END``
+  *
+    * ``NUM_BEGIN``
+  *
+    * ``NUM_INSERT``
+  *
+    * ``NUM_DELETE``
+  *
+    * ``NUM_EQUAL``
+  *
+    * ``NUM_MULTIPLY``
+  *
+    * ``NUM_ADD``
+  *
+    * ``NUM_SEPARATOR``
+  *
+    * ``NUM_SUBTRACT``
+  *
+    * ``NUM_DECIMAL``
+  *
+    * ``NUM_DIVIDE``
+  *
+    * ``NUM_0``
+  *
+    * ``NUM_1``
+  *
+    * ``NUM_2``
+  *
+    * ``NUM_3``
+  *
+    * ``NUM_4``
+  *
+    * ``NUM_5``
+  *
+    * ``NUM_6``
+  *
+    * ``NUM_7``
+  *
+    * ``NUM_8``
+  *
+    * ``NUM_9``
+
+Function keys
+^^^^^^^^^^^^^
+
+.. list-table::
+
+  *
+    * ``F1``
+  *
+    * ``F2``
+  *
+    * ``F3``
+  *
+    * ``F4``
+  *
+    * ``F5``
+  *
+    * ``F6``
+  *
+    * ``F7``
+  *
+    * ``F8``
+  *
+    * ``F9``
+  *
+    * ``F10``
+  *
+    * ``F11``
+  *
+    * ``F12``
+  *
+    * ``F13``
+  *
+    * ``F14``
+  *
+    * ``F15``
+  *
+    * ``F16``
+  *
+    * ``F17``
+  *
+    * ``F18``
+  *
+    * ``F19``
+  *
+    * ``F20``
+
+Modifiers
+^^^^^^^^^
+
+.. list-table::
+
+  *
+    * ``LSHIFT``
+  *
+    * ``RSHIFT``
+  *
+    * ``LCTRL``
+  *
+    * ``RCTRL``
+  *
+    * ``CAPSLOCK``
+  *
+    * ``LMETA``
+  *
+    * ``RMETA``
+  *
+    * ``LALT``
+  *
+    * ``RALT``
+  *
+    * ``LWINDOWS``
+  *
+    * ``RWINDOWS``
+  *
+    * ``LCOMMAND``
+  *
+    * ``RCOMMAND``
+  *
+    * ``LOPTION``
+  *
+    * ``ROPTION``
+
+Latin-1
+^^^^^^^
+
+.. list-table::
+
+  *
+    * ``SPACE``
+  *
+    * ``EXCLAMATION``
+  *
+    * ``DOUBLEQUOTE``
+  *
+    * ``HASH``
+  *
+    * ``POUND``
+  *
+    * ``DOLLAR``
+  *
+    * ``PERCENT``
+  *
+    * ``AMPERSAND``
+  *
+    * ``APOSTROPHE``
+  *
+    * ``PARENLEFT``
+  *
+    * ``PARENRIGHT``
+  *
+    * ``ASTERISK``
+  *
+    * ``PLUS``
+  *
+    * ``COMMA``
+  *
+    * ``MINUS``
+  *
+    * ``PERIOD``
+  *
+    * ``SLASH``
+  *
+    * ``_0``
+  *
+    * ``_1``
+  *
+    * ``_2``
+  *
+    * ``_3``
+  *
+    * ``_4``
+  *
+    * ``_5``
+  *
+    * ``_6``
+  *
+    * ``_7``
+  *
+    * ``_8``
+  *
+    * ``_9``
+  *
+    * ``COLON``
+  *
+    * ``SEMICOLON``
+  *
+    * ``LESS``
+  *
+    * ``EQUAL``
+  *
+    * ``GREATER``
+  *
+    * ``QUESTION``
+  *
+    * ``AT``
+  *
+    * ``BRACKETLEFT``
+  *
+    * ``BACKSLASH``
+  *
+    * ``BRACKETRIGHT``
+  *
+    * ``ASCIICIRCUM``
+  *
+    * ``UNDERSCORE``
+  *
+    * ``GRAVE``
+  *
+    * ``QUOTELEFT``
+  *
+    * ``A``
+  *
+    * ``B``
+  *
+    * ``C``
+  *
+    * ``D``
+  *
+    * ``E``
+  *
+    * ``F``
+  *
+    * ``G``
+  *
+    * ``H``
+  *
+    * ``I``
+  *
+    * ``J``
+  *
+    * ``K``
+  *
+    * ``L``
+  *
+    * ``M``
+  *
+    * ``N``
+  *
+    * ``O``
+  *
+    * ``P``
+  *
+    * ``Q``
+  *
+    * ``R``
+  *
+    * ``S``
+  *
+    * ``T``
+  *
+    * ``U``
+  *
+    * ``V``
+  *
+    * ``W``
+  *
+    * ``X``
+  *
+    * ``Y``
+  *
+    * ``Z``
+  *
+    * ``BRACELEFT``
+  *
+    * ``BAR``
+  *
+    * ``BRACERIGHT``
+  *
+    * ``ASCIITILDE``
diff --git a/doc/modules/window_mouse.rst b/doc/modules/window_mouse.rst
new file mode 100644
index 0000000..b3981ec
--- /dev/null
+++ b/doc/modules/window_mouse.rst
@@ -0,0 +1,5 @@
+pyglet.window.mouse
+===================
+
+.. automodule:: pyglet.window.mouse
+    :members:
diff --git a/doc/programming_guide/advanced.rst b/doc/programming_guide/advanced.rst
new file mode 100644
index 0000000..27b4f0d
--- /dev/null
+++ b/doc/programming_guide/advanced.rst
@@ -0,0 +1,30 @@
+Advanced topics
+===============
+
+.. _guide_environment-settings:
+
+Environment settings
+--------------------
+
+Options in the :py:attr:`pyglet.options` dictionary can have defaults set
+through the operating system's environment variable.  The following table
+shows which environment variable is used for each option:
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Environment variable
+          - :py:attr:`pyglet.options` key
+          - Type
+          - Default value
+        * - ``PYGLET_AUDIO``
+          - ``audio``
+          - List of strings
+          - ``directsound,openal,alsa,silent``
+        * - ``PYGLET_DEBUG_GL``
+          - ``debug_gl``
+          - Boolean
+          - ``1`` [#debug_gl]_
+
+.. [#debug_gl] Defaults to ``1`` unless Python is run with ``-O`` or from a
+    frozen executable.
diff --git a/doc/programming_guide/context.rst b/doc/programming_guide/context.rst
new file mode 100644
index 0000000..49c18d8
--- /dev/null
+++ b/doc/programming_guide/context.rst
@@ -0,0 +1,424 @@
+Creating an OpenGL context
+==========================
+
+This section describes how to configure an OpenGL context.  For most
+applications the information described here is far too low-level to be of any
+concern, however more advanced applications can take advantage of the complete
+control pyglet provides.
+
+Displays, screens, configs and contexts
+---------------------------------------
+
+.. figure:: img/context_flow.png
+
+    Flow of construction, from the abstract Canvas to a newly
+    created Window with its Context.
+
+Contexts and configs
+^^^^^^^^^^^^^^^^^^^^
+
+When you draw on a window in pyglet, you are drawing to an OpenGL context.
+Every window has its own context, which is created when the window is created.
+You can access the window's context via its
+:attr:`~pyglet.window.Window.context` attribute.
+
+The context is created from an OpenGL configuration (or "config"), which
+describes various properties of the context such as what color format to use,
+how many buffers are available, and so on.  You can access the config
+that was used to create a context via the context's
+:attr:`~pyglet.gl.Context.config` attribute.
+
+For example, here we create a window using the default config and examine some
+of its properties::
+
+    >>> import pyglet
+    >>> window = pyglet.window.Window()
+    >>> context = window.context
+    >>> config = context.config
+    >>> config.double_buffer
+    c_int(1)
+    >>> config.stereo
+    c_int(0)
+    >>> config.sample_buffers
+    c_int(0)
+
+Note that the values of the config's attributes are all ctypes instances.
+This is because the config was not specified by pyglet.  Rather, it has been
+selected by pyglet from a list of configs supported by the system.  You can
+make no guarantee that a given config is valid on a system unless it was
+provided to you by the system.
+
+pyglet simplifies the process of selecting one of the system's configs by
+allowing you to create a "template" config which specifies only the values you
+are interested in.  See :ref:`guide_simple-context-configuration` for details.
+
+
+.. _guide_displays:
+
+Displays
+^^^^^^^^
+
+The system may actually support several different sets of configs, depending on
+which display device is being used.  For example, a computer with two video
+cards may not support the same configs on each card.  Another example is using
+X11 remotely: the display device will support different configurations than the
+local driver.  Even a single video card on the local computer may support
+different configs for two monitors plugged in.
+
+In pyglet, a :class:`~pyglet.canvas.Display` is a collection of "screens"
+attached to a single display device.  On Linux, the display device corresponds
+to the X11 display being used.  On Windows and Mac OS X, there is only one
+display (as these operating systems present multiple video cards as a single
+virtual device).
+
+The :mod:`pyglet.canvas` module provides access to the display(s). Use the
+:func:`~pyglet.canvas.get_display` function to get the default display::
+
+    >>> display = pyglet.canvas.get_display()
+
+.. note::
+
+    On X11, you can use the :class:`~pyglet.canvas.Display` class directly to
+    specify the display string to use, for example to use a remotely connected
+    display.  The name string is in the same format as used by the ``DISPLAY``
+    environment variable::
+
+        >>> display = pyglet.canvas.Display(name=':1')
+
+    If you have multiple physical screens and you're using Xinerama, see
+    :ref:`guide_screens` to select the desired screen as you would for Windows
+    and Mac OS X. Otherwise, you can specify the screen number via the
+    ``x_screen`` argument::
+
+        >>> display = pyglet.canvas.Display(name=':1', x_screen=1)
+
+.. _guide_screens:
+
+Screens
+^^^^^^^
+
+Once you have obtained a display, you can enumerate the screens that are
+connected.  A screen is the physical display medium connected to the display
+device; for example a computer monitor, TV or projector.  Most computers will
+have a single screen, however dual-head workstations and laptops connected to
+a projector are common cases where more than one screen will be present.
+
+In the following example the screens of a dual-head workstation are listed::
+
+    >>> for screen in display.get_screens():
+    ...     print(screen)
+    ...
+    XlibScreen(screen=0, x=1280, y=0, width=1280, height=1024, xinerama=1)
+    XlibScreen(screen=0, x=0, y=0, width=1280, height=1024, xinerama=1)
+
+Because this workstation is running Linux, the returned screens are
+``XlibScreen``, a subclass of :class:`~pyglet.canvas.Screen`. The
+``screen`` and ``xinerama`` attributes are specific to Linux, but the
+:attr:`~pyglet.canvas.Screen.x`, :attr:`~pyglet.canvas.Screen.y`,
+:attr:`~pyglet.canvas.Screen.width` and
+:attr:`~pyglet.canvas.Screen.height` attributes are present on all screens,
+and describe the screen's geometry, as shown below.
+
+.. figure:: img/screens.png
+
+    Example arrangement of screens and their reported geometry.  Note that the
+    primary display (marked "1") is positioned on the right, according to this
+    particular user's preference.
+
+There is always a "default" screen, which is the first screen returned by
+:meth:`~pyglet.canvas.Display.get_screens`.  Depending on the operating system,
+the default screen is usually the one that contains the taskbar (on Windows) or
+menu bar (on OS X).
+You can access this screen directly using
+:meth:`~pyglet.canvas.Display.get_default_screen`.
+
+
+.. _guide_glconfig:
+
+OpenGL configuration options
+----------------------------
+
+When configuring or selecting a :class:`~pyglet.gl.Config`, you do so based
+on the properties of that config.  pyglet supports a fixed subset of the
+options provided by AGL, GLX, WGL and their extensions.  In particular, these
+constraints are placed on all OpenGL configs:
+
+* Buffers are always component (RGB or RGBA) color, never palette indexed.
+* The "level" of a buffer is always 0 (this parameter is largely unsupported
+  by modern OpenGL drivers anyway).
+* There is no way to set the transparent color of a buffer (again, this
+  GLX-specific option is not well supported).
+* There is no support for pbuffers (equivalent functionality can be achieved
+  much more simply and efficiently using framebuffer objects).
+
+The visible portion of the buffer, sometimes called the color buffer, is
+configured with the following attributes:
+
+    ``buffer_size``
+        Number of bits per sample.  Common values are 24 and 32, which each
+        dedicate 8 bits per color component.  A buffer size of 16 is also
+        possible, which usually corresponds to 5, 6, and 5 bits of red, green
+        and blue, respectively.
+
+        Usually there is no need to set this property, as the device driver
+        will select a buffer size compatible with the current display mode
+        by default.
+    ``red_size``, ``blue_size``, ``green_size``, ``alpha_size``
+        These each give the number of bits dedicated to their respective color
+        component.  You should avoid setting any of the red, green or blue
+        sizes, as these are determined by the driver based on the
+        ``buffer_size`` property.
+
+        If you require an alpha channel in your color buffer (for example, if
+        you are compositing in multiple passes) you should specify
+        ``alpha_size=8`` to ensure that this channel is created.
+    ``sample_buffers`` and ``samples``
+        Configures the buffer for multisampling (MSAA), in which more than
+        one color sample is used to determine the color of each pixel,
+        leading to a higher quality, antialiased image.
+
+        Enable multisampling (MSAA) by setting ``sample_buffers=1``, then
+        give the number of samples per pixel to use in ``samples``.
+        For example, ``samples=2`` is the fastest, lowest-quality multisample
+        configuration. ``samples=4`` is still widely supported
+        and fairly performant even on Intel HD and AMD Vega.
+        Most modern GPUs support 2×, 4×, 8×, and 16× MSAA samples
+        with fairly high performance.
+
+    ``stereo``
+        Creates separate left and right buffers, for use with stereo hardware.
+        Only specialised video hardware such as stereoscopic glasses will
+        support this option.  When used, you will need to manually render to
+        each buffer, for example using `glDrawBuffers`.
+    ``double_buffer``
+        Create separate front and back buffers.  Without double-buffering,
+        drawing commands are immediately visible on the screen, and the user
+        will notice a visible flicker as the image is redrawn in front of
+        them.
+
+        It is recommended to set ``double_buffer=True``, which creates a
+        separate hidden buffer to which drawing is performed.  When the
+        `Window.flip` is called, the buffers are swapped,
+        making the new drawing visible virtually instantaneously.
+
+In addition to the color buffer, several other buffers can optionally be
+created based on the values of these properties:
+
+    ``depth_size``
+        A depth buffer is usually required for 3D rendering.  The typical
+        depth size is 24 bits.  Specify ``0`` if you do not require a depth
+        buffer.
+    ``stencil_size``
+        The stencil buffer is required for masking the other buffers and
+        implementing certain volumetric shadowing algorithms.  The typical
+        stencil size is 8 bits; or specify ``0`` if you do not require it.
+    ``accum_red_size``, ``accum_blue_size``, ``accum_green_size``, ``accum_alpha_size``
+        The accumulation buffer can be used for simple antialiasing,
+        depth-of-field, motion blur and other compositing operations.  Its use
+        nowadays is being superceded by the use of floating-point textures,
+        however it is still a practical solution for implementing these
+        effects on older hardware.
+
+        If you require an accumulation buffer, specify ``8`` for each
+        of these attributes (the alpha component is optional, of course).
+    ``aux_buffers``
+        Each auxiliary buffer is configured the same as the colour buffer.
+        Up to four auxiliary buffers can typically be created.  Specify ``0``
+        if you do not require any auxiliary buffers.
+
+        Like the accumulation buffer, auxiliary buffers are used less often
+        nowadays as more efficient techniques such as render-to-texture are
+        available.  They are almost universally available on older hardware,
+        though, where the newer techniques are not possible.
+
+If you wish to work with OpenGL directly, you can request a higher level
+context. This is required if you wish to work with the modern OpenGL
+programmable pipeline. Please note, however, that pyglet currently uses
+legacy OpenGL functionality for many of it's internal modules (such as
+the text, graphics, and sprite modules). Requesting a higher version
+context will currently prevent usage of these modules.
+
+    ``major_version``
+        This will be either 3 or 4, for an OpenGL 3.x or 4.x context.
+    ``minor_version``
+        The requested minor version of the context. In some cases, the OpenGL
+        driver may return a higher version than requested.
+    ``forward_compatible``
+        Setting this to `True` will ask the driver to exclude legacy OpenGL
+        features from the context. Khronos does not recommend this option.
+
+.. note::
+   To request a higher higher version OpenGL context on Mac OSX, it is necessary
+   to disable the pyglet shadow context. To do this, set the pyglet option
+   ``pyglet.options['shadow_window']`` to ``False`` `before` creating a Window,
+   or importing ``pyglet.window``.
+
+The default configuration
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If you create a :class:`~pyglet.window.Window` without specifying the context
+or config, pyglet will use a template config with the following properties:
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Attribute
+          - Value
+        * - double_buffer
+          - True
+        * - depth_size
+          - 24
+
+.. _guide_simple-context-configuration:
+
+Simple context configuration
+----------------------------
+
+A context can only be created from a config that was provided by the system.
+Enumerating and comparing the attributes of all the possible configs is
+a complicated process, so pyglet provides a simpler interface based on
+"template" configs.
+
+To get the config with the attributes you need, construct a
+:class:`~pyglet.gl.Config` and set only the attributes you are interested in.
+You can then supply this config to the :class:`~pyglet.window.Window`
+constructor to create the context.
+
+For example, to create a window with an alpha channel::
+
+    config = pyglet.gl.Config(alpha_size=8)
+    window = pyglet.window.Window(config=config)
+
+It is sometimes necessary to create the context yourself, rather than letting
+the :class:`~pyglet.window.Window` constructor do this for you.  In this case
+use :meth:`~pyglet.canvas.Screen.get_best_config` to obtain a "complete"
+config, which you can then use to create the context::
+
+    display = pyglet.canvas.get_display()
+    screen = display.get_default_screen()
+
+    template = pyglet.gl.Config(alpha_size=8)
+    config = screen.get_best_config(template)
+    context = config.create_context(None)
+    window = pyglet.window.Window(context=context)
+
+Note that you cannot create a context directly from a template (any
+:class:`~pyglet.gl.Config` you constructed yourself).  The
+:class:`~pyglet.window.Window` constructor performs a similar process to the
+above to create the context if a template config is given.
+
+Not all configs will be possible on all machines.  The call to
+:meth:`~pyglet.canvas.Screen.get_best_config` will raise
+:class:`~pyglet.window.NoSuchConfigException` if the hardware does not
+support the requested attributes.  It will never return a config that does not
+meet or exceed the attributes you specify in the template.
+
+You can use this to support newer hardware features where available, but also
+accept a lesser config if necessary.  For example, the following code creates
+a window with multisampling if possible, otherwise leaves multisampling off::
+
+    template = pyglet.gl.Config(sample_buffers=1, samples=4)
+    try:
+        config = screen.get_best_config(template)
+    except pyglet.window.NoSuchConfigException:
+        template = gl.Config()
+        config = screen.get_best_config(template)
+    window = pyglet.window.Window(config=config)
+
+Selecting the best configuration
+--------------------------------
+
+Allowing pyglet to select the best configuration based on a template is
+sufficient for most applications, however some complex programs may want to
+specify their own algorithm for selecting a set of OpenGL attributes.
+
+You can enumerate a screen's configs using the
+:meth:`~pyglet.canvas.Screen.get_matching_configs` method. You must supply a
+template as a minimum specification, but you can supply an "empty" template
+(one with no attributes set) to get a list of all configurations supported by
+the screen.
+
+In the following example, all configurations with either an auxiliary buffer
+or an accumulation buffer are printed::
+
+    display = pyglet.canvas.get_display()
+    screen = display.get_default_screen()
+
+    for config in screen.get_matching_configs(gl.Config()):
+        if config.aux_buffers or config.accum_red_size:
+            print(config)
+
+As well as supporting more complex configuration selection algorithms,
+enumeration allows you to efficiently find the maximum value of an attribute
+(for example, the maximum samples per pixel), or present a list of possible
+configurations to the user.
+
+Sharing objects between contexts
+--------------------------------
+
+Every window in pyglet has its own OpenGL context.  Each context has its own
+OpenGL state, including the matrix stacks and current flags.  However,
+contexts can optionally share their objects with one or more other contexts.
+Shareable objects include:
+
+* Textures
+* Display lists
+* Shader programs
+* Vertex and pixel buffer objects
+* Framebuffer objects
+
+There are two reasons for sharing objects.  The first is to allow objects to
+be stored on the video card only once, even if used by more than one window.
+For example, you could have one window showing the actual game, with other
+"debug" windows showing the various objects as they are manipulated.  Or, a
+set of widget textures required for a GUI could be shared between all the
+windows in an application.
+
+The second reason is to avoid having to recreate the objects when a context
+needs to be recreated.  For example, if the user wishes to turn on
+multisampling, it is necessary to recreate the context.  Rather than destroy
+the old one and lose all the objects already created, you can
+
+1. Create the new context, sharing object space with the old context, then
+2. Destroy the old context.  The new context retains all the old objects.
+
+pyglet defines an :class:`~pyglet.gl.ObjectSpace`: a representation of a
+collection of objects used by one or more contexts.  Each context has a single
+object space, accessible via its
+:py:attr:`~pyglet.gl.base.Context.object_space` attribute.
+
+By default, all contexts share the same object space as long as at least one
+context using it is "alive".  If all the contexts sharing an object space are
+lost or destroyed, the object space will be destroyed also.  This is why it is
+necessary to follow the steps outlined above for retaining objects when a
+context is recreated.
+
+pyglet creates a hidden "shadow" context as soon as :mod:`pyglet.gl` is
+imported. By default, all windows will share object space with this shadow
+context, so the above steps are generally not needed. The shadow context also
+allows objects such as textures to be loaded before a window is created (see
+``shadow_window`` in :data:`pyglet.options` for further details).
+
+When you create a :class:`~pyglet.gl.Context`, you tell pyglet which other
+context it will obtain an object space from.  By default (when using the
+:class:`~pyglet.window.Window` constructor
+to create the context) the most recently created context will be used.  You
+can specify another context, or specify no context (to create a new object
+space) in the :class:`~pyglet.gl.Context` constructor.
+
+It can be useful to keep track of which object space an object was created in.
+For example, when you load a font, pyglet caches the textures used and reuses
+them; but only if the font is being loaded on the same object space.  The
+easiest way to do this is to set your own attributes on the
+:py:class:`~pyglet.gl.ObjectSpace` object.
+
+In the following example, an attribute is set on the object space indicating
+that game objects have been loaded.  This way, if the context is recreated,
+you can check for this attribute to determine if you need to load them again::
+
+    context = pyglet.gl.current_context
+    object_space = context.object_space
+    object_space.my_game_objects_loaded = True
+
+Avoid using attribute names on :class:`~pyglet.gl.ObjectSpace` that begin with
+``"pyglet"``, as they may conflict with an internal module.
diff --git a/doc/programming_guide/debug.rst b/doc/programming_guide/debug.rst
new file mode 100644
index 0000000..d4a424c
--- /dev/null
+++ b/doc/programming_guide/debug.rst
@@ -0,0 +1,153 @@
+Debugging tools
+===============
+
+pyglet includes a number of debug paths that can be enabled during or before
+application startup.  These were primarily developed to aid in debugging
+pyglet itself, however some of them may also prove useful for understanding
+and debugging pyglet applications.
+
+Each debug option is a key in the :py:attr:`pyglet.options` dictionary.
+Options can be set directly on the dictionary before any other modules
+are imported::
+
+    import pyglet
+    pyglet.options['debug_gl'] = False
+
+They can also be set with environment variables before pyglet is imported.
+The corresponding environment variable for each option is the string
+``PYGLET_`` prefixed to the uppercase option key.  For example, the
+environment variable for ``debug_gl`` is ``PYGLET_DEBUG_GL``.  Boolean options
+are set or unset with ``1`` and ``0`` values.
+
+A summary of the debug environment variables appears in the table below.
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Option
+          - Environment variable
+          - Type
+        * - ``debug_font``
+          - ``PYGLET_DEBUG_FONT``
+          - bool
+        * - ``debug_gl``
+          - ``PYGLET_DEBUG_GL``
+          - bool
+        * - ``debug_gl_trace``
+          - ``PYGLET_DEBUG_GL_TRACE``
+          - bool
+        * - ``debug_gl_trace_args``
+          - ``PYGLET_DEBUG_GL_TRACE_ARGS``
+          - bool
+        * - ``debug_graphics_batch``
+          - ``PYGLET_DEBUG_GRAPHICS_BATCH``
+          - bool
+        * - ``debug_lib``
+          - ``PYGLET_DEBUG_LIB``
+          - bool
+        * - ``debug_media``
+          - ``PYGLET_DEBUG_MEDIA``
+          - bool
+        * - ``debug_trace``
+          - ``PYGLET_DEBUG_TRACE``
+          - bool
+        * - ``debug_trace_args``
+          - ``PYGLET_DEBUG_TRACE_ARGS``
+          - bool
+        * - ``debug_trace_depth``
+          - ``PYGLET_DEBUG_TRACE_DEPTH``
+          - int
+        * - ``debug_win32``
+          - ``PYGLET_DEBUG_WIN32``
+          - bool
+        * - ``debug_x11``
+          - ``PYGLET_DEBUG_X11``
+          - bool
+        * - ``graphics_vbo``
+          - ``PYGLET_GRAPHICS_VBO``
+          - bool
+
+The ``debug_media`` and ``debug_font`` options are used to debug the
+:py:mod:`pyglet.media` and :py:mod:`pyglet.font` modules, respectively.
+Their behaviour is platform-dependent and useful only for pyglet developers.
+
+The remaining debug options are detailed below.
+
+Debugging OpenGL
+----------------
+
+The ``graphics_vbo`` option enables the use of vertex buffer objects in
+:py:mod:`pyglet.graphics` (instead, only vertex arrays).  This is useful when
+debugging the ``graphics`` module as well as isolating code for determining if
+a video driver is faulty.
+
+The ``debug_graphics_batch`` option causes all
+:py:class:`~pyglet.graphics.Batch` objects to dump their
+rendering tree to standard output before drawing, after any change (so two
+drawings of the same tree will only dump once).  This is useful to debug
+applications making use of :py:class:`~pyglet.graphics.Group` and
+:py:class:`~pyglet.graphics.Batch` rendering.
+
+Error checking
+^^^^^^^^^^^^^^
+
+The ``debug_gl`` option intercepts most OpenGL calls and calls ``glGetError``
+afterwards (it only does this where such a call would be legal).  If an error
+is reported, an exception is raised immediately.
+
+This option is enabled by default unless the ``-O`` flag (optimisation) is
+given to Python, or the script is running from within a py2exe or py2app
+package.
+
+Tracing
+^^^^^^^
+
+The ``debug_gl_trace`` option causes all OpenGL functions called to be dumped
+to standard out.  When combined with ``debug_gl_trace_args``, the arguments
+given to each function are also printed (they are abbreviated if necessary to
+avoid dumping large amounts of buffer data).
+
+Tracing execution
+-----------------
+
+The ``debug_trace`` option enables Python-wide function tracing.  This causes
+every function call to be printed to standard out.  Due to the large number of
+function calls required just to initialise pyglet, it is recommended to
+redirect standard output to a file when using this option.
+
+The ``debug_trace_args`` option additionally prints the arguments to each
+function call.
+
+When ``debug_trace_depth`` is greater than 1 the caller(s) of each function
+(and their arguments, if ``debug_trace_args`` is set) are also printed.  Each
+caller is indented beneath the callee.  The default depth is 1, specifying
+that no callers are printed.
+
+Platform-specific debugging
+---------------------------
+
+The ``debug_lib`` option causes the path of each loaded library to be printed
+to standard out.  This is performed by the undocumented ``pyglet.lib`` module,
+which on Linux and Mac OS X must sometimes follow complex procedures to find
+the correct library.  On Windows not all libraries are loaded via this module,
+so they will not be printed (however, loading Windows DLLs is sufficiently
+simple that there is little need for this information).
+
+Linux
+^^^^^
+
+X11 errors are caught by pyglet and suppressed, as there are plenty of X
+servers in the wild that generate errors that can be safely ignored.
+The ``debug_x11`` option causes these errors to be dumped to standard out,
+along with a traceback of the Python stack (this may or may not correspond to
+the error, depending on whether or not it was reported asynchronously).
+
+Windows
+^^^^^^^
+
+The ``debug_win32`` option causes all library calls into ``user32.dll``,
+``kernel32.dll`` and ``gdi32.dll`` to be intercepted.  Before each library
+call ``SetLastError(0)`` is called, and afterwards ``GetLastError()`` is
+called.  Any errors discovered are written to a file named
+``debug_win32.log``.  Note that an error is only valid if the function called
+returned an error code, but the interception function does not check this.
diff --git a/doc/programming_guide/eventloop.rst b/doc/programming_guide/eventloop.rst
new file mode 100644
index 0000000..1fb2e5e
--- /dev/null
+++ b/doc/programming_guide/eventloop.rst
@@ -0,0 +1,139 @@
+.. _programming-guide-eventloop:
+
+The application event loop
+==========================
+
+In order to let pyglet process operating system events such as mouse and
+keyboard events, applications need to enter an application event loop.  The
+event loop continuously checks for new events, dispatches those events, and
+updates the contents of all open windows.
+
+pyglet provides an application event loop that is tuned for performance and
+low power usage on Windows, Linux and Mac OS X.  Most applications need only
+call::
+
+    pyglet.app.run()
+
+to enter the event loop after creating their initial set of windows and
+attaching event handlers. The :py:func:`~pyglet.app.run` function does not
+return until all open windows have been closed, or until
+:py:func:`pyglet.app.exit()` is called.
+
+The pyglet application event loop dispatches window events (such as for mouse
+and keyboard input) as they occur and dispatches the
+:py:meth:`~pyglet.window.Window.on_draw` event to
+each window after every iteration through the loop.
+
+To have additional code run periodically or every iteration through the loop,
+schedule functions on the clock (see
+:ref:`guide_calling-functions-periodically`). pyglet ensures that the loop
+iterates only as often as necessary
+to fulfill all scheduled functions and user input.
+
+Customising the event loop
+--------------------------
+
+The pyglet event loop is encapsulated in the
+:py:class:`~pyglet.app.EventLoop` class, which provides
+several hooks that can be overridden for customising its behaviour.  This is
+recommended only for advanced users -- typical applications and games are
+unlikely to require this functionality.
+
+To use the :py:class:`~pyglet.app.EventLoop` class directly, instantiate it and call `run`::
+
+    event_loop = pyglet.app.EventLoop()
+    event_loop.run()
+
+Only one :py:class:`~pyglet.app.EventLoop` can be running at a time; when the
+:py:meth:`~pyglet.app.EventLoop.run` method is called
+the module variable :py:attr:`pyglet.app.event_loop` is set to the running
+instance. Other pyglet modules such as :py:mod:`pyglet.window` depend on this.
+
+Event loop events
+^^^^^^^^^^^^^^^^^
+
+You can listen for several events on the event loop instance.  The most useful
+of these is :py:meth:`~pyglet.app.EventLoop.on_window_close`, which is
+dispatched whenever a window is closed.  The default handler for this event
+exits the event loop if there are no more windows.  The following example
+overrides this behaviour to exit the application whenever any window is
+closed::
+
+    event_loop = pyglet.app.EventLoop()
+
+    @event_loop.event
+    def on_window_close(window):
+        event_loop.exit()
+        return pyglet.event.EVENT_HANDLED
+
+    event_loop.run()
+
+Overriding the default idle policy
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The :py:meth:`pyglet.app.EventLoop.idle` method is called every iteration of
+the event loop.  It is responsible for calling scheduled clock functions,
+redrawing windows, and deciding how idle the application is. You can override
+this method if you have specific requirements for tuning the performance
+of your application; especially if it uses many windows.
+
+The default implementation has the following algorithm:
+
+1. Call :py:func:`pyglet.clock.tick` with ``poll=True`` to call any scheduled
+   functions.
+2. Dispatch the :py:meth:`~pyglet.window.Window.on_draw` event and call
+   :py:meth:`~pyglet.window.Window.flip` on every open window.
+3. Return the value of :py:func:`pyglet.clock.get_sleep_time`.
+
+The return value of the :py:meth:`~pyglet.clock.get_sleep_time` method is
+the number of seconds until the event loop needs to iterate again (unless
+there is an earlier user-input event); or ``None`` if the loop can wait
+for input indefinitely.
+
+Note that this default policy causes every window to be redrawn during every
+user event -- if you have more knowledge about which events have an effect on
+which windows you can improve on the performance of this method.
+
+Dispatching events manually
+---------------------------
+
+Earlier versions of pyglet and certain other windowing toolkits such as
+PyGame and SDL require the application developer to write their own event
+loop. This is usually just an inconvenience compared to
+:py:func:`pyglet.app.run`, but can be necessary in some situations when
+combining pyglet with other toolkits.
+
+A simple event loop usually has the following form::
+
+    while True:
+        pyglet.clock.tick()
+
+        for window in pyglet.app.windows:
+            window.switch_to()
+            window.dispatch_events()
+            window.dispatch_event('on_draw')
+            window.flip()
+
+The :py:meth:`~pyglet.window.Window.dispatch_events` method checks the window's
+operating system event queue for user input and dispatches any events found.
+The method does not wait for input -- if ther are no events pending, control is
+returned to the program immediately.
+
+The call to :py:func:`pyglet.clock.tick` is required for ensuring scheduled
+functions are called, including the internal data pump functions for playing
+sounds, animations, and video.
+
+While it is possible to write your own event loop in this way, it is strongly
+discouraged for the following reasons:
+
+* The :py:class:`~pyglet.app.EventLoop` class provides plenty of hooks for most
+  toolkits to be integrated without needing to resort to a manual event loop.
+* Because :py:class:`~pyglet.app.EventLoop` is tuned for specific operating
+  systems, it is more responsive to user events, and continues calling clock
+  functions while windows are being resized, and (on Mac OS X) the menu bar is
+  being tracked.
+* It is difficult to write a manual event loop that does not consume
+  100% CPU while still remaining responsive to user input.
+
+The capability for writing manual event loops remains for legacy support and
+extreme cases where the developer knows what they are doing.
diff --git a/doc/programming_guide/events.rst b/doc/programming_guide/events.rst
new file mode 100644
index 0000000..1f088e8
--- /dev/null
+++ b/doc/programming_guide/events.rst
@@ -0,0 +1,278 @@
+The pyglet event framework
+==========================
+
+The :py:mod:`pyglet.window`, :py:mod:`pyglet.media`, :py:mod:`pyglet.app`,
+:py:mod:`pyglet.text`, :py:mod:`pyglet.input` and other modules make use
+of a consistent event pattern.  This provides several ways to attach event
+handlers to objects.  You can also reuse this pattern in your own
+classes easily, by subclassing :py:class:`~pyglet.event.EventDispatcher`.
+
+Throughout this documentation, an "event dispatcher" is an object that has
+events it needs to notify other objects about, and an "event handler" is some
+code that can be attached to a dispatcher.
+
+Setting event handlers
+----------------------
+
+An event handler is simply a function with a formal parameter list
+corresponding to the event type. For example, the
+:py:meth:`pyglet.window.Window.on_resize` event has the parameters
+``(width, height)``, so an event handler for this event could be written as::
+
+    def on_resize(width, height):
+        pass
+
+The :py:class:`~pyglet.window.Window` class subclasses
+:py:class:`~pyglet.event.EventDispatcher`, which enables it to dispatch
+its own events.  There are a few different ways in which event handlers
+can be attached to recieve them. The simplest way is to directly attach the
+event handler to the corresponding attribute on the object.  This will
+completely replace the default event handler::
+
+    window = pyglet.window.Window()
+
+    def on_resize(width, height):
+        pass
+    window.on_resize = on_resize
+
+If you don't want to replace the default event handler, but instead want to
+add an additional one, pyglet provides a shortcut using the
+:py:class:`~pyglet.event.EventDispatcher.event` decorator.
+Your custom event handler will run, followed by the default event handler::
+
+    window = window.Window()
+
+    @window.event
+    def on_resize(width, height):
+        pass
+
+or if your handler has a different name::
+
+    @window.event('on_resize')
+    def my_resize_handler(width, height):
+        pass
+
+In some cases, replacing the default event handler may be desired.
+For example, the default :py:meth:`pyglet.window.Window.on_resize` event
+sets up a 2D orthographic OpenGL projection. If you wish to use another
+OpenGL projection, such as for a 3D scene, then you will likely want
+to replace this with your own custom event handler.
+
+In most simple cases, the :py:class:`~pyglet.event.EventDispatcher.event`
+decorator is most convienent.  One limitation of using the decorator,
+however, is that you can only add one additional event handler.
+If you want to add multiple additional event handlers, the next section
+describes how to accomplish that.
+
+As a quick note, as shown in :ref:`guide_subclassing-window`,
+you can also replace default event handlers by subclassing the event
+dispatcher and adding the event handler as a method::
+
+    class MyWindow(pyglet.window.Window):
+        def on_resize(self, width, height):
+            pass
+
+Stacking event handlers
+-----------------------
+
+It is often convenient to attach more than one event handler for an event.
+:py:class:`~pyglet.event.EventDispatcher` allows you to stack event handlers
+upon one another, rather than replacing them outright. The event will
+propagate from the top of the stack to the bottom, but can be stopped
+by any handler along the way.
+
+To push an event handler onto the stack,
+use the :py:meth:`~pyglet.event.EventDispatcher.push_handlers` method::
+
+    def on_key_press(symbol, modifiers):
+        if symbol == key.SPACE:
+            fire_laser()
+
+    window.push_handlers(on_key_press)
+
+One use for pushing handlers instead of setting them is to handle different
+parameterisations of events in different functions.  In the above example, if
+the spacebar is pressed, the laser will be fired.  After the event handler
+returns control is passed to the next handler on the stack, which on a
+:py:class:`~pyglet.window.Window` is a function that checks for the ESC key
+and sets the ``has_exit`` attribute if it is pressed.  By pushing the event
+handler instead of setting it, the application keeps the default behaviour
+while adding additional functionality.
+
+You can prevent the remaining event handlers in the stack from receiving the
+event by returning a true value.  The following event handler, when pushed
+onto the window, will prevent the escape key from exiting the program::
+
+    def on_key_press(symbol, modifiers):
+        if symbol == key.ESCAPE:
+            return True
+
+    window.push_handlers(on_key_press)
+
+You can push more than one event handler at a time, which is especially useful
+when coupled with the :py:meth:`~pyglet.event.EventDispatcher.pop_handlers`
+function. In the following example, when the game starts some additional
+event handlers are pushed onto the stack. When the game ends (perhaps
+returning to some menu screen) the handlers are popped off in one go::
+
+    def start_game():
+        def on_key_press(symbol, modifiers):
+            print('Key pressed in game')
+            return True
+
+        def on_mouse_press(x, y, button, modifiers):
+            print('Mouse button pressed in game')
+            return True
+
+        window.push_handlers(on_key_press, on_mouse_press)
+
+    def end_game():
+        window.pop_handlers()
+
+Note that you do not specify which handlers to pop off the stack -- the entire
+top "level" (consisting of all handlers specified in a single call to
+:py:meth:`~pyglet.event.EventDispatcher.push_handlers`) is popped.
+
+You can apply the same pattern in an object-oriented fashion by grouping
+related event handlers in a single class.  In the following example, a
+``GameEventHandler`` class is defined.  An instance of that class can be
+pushed on and popped off of a window::
+
+    class GameEventHandler:
+        def on_key_press(self, symbol, modifiers):
+            print('Key pressed in game')
+            return True
+
+        def on_mouse_press(self, x, y, button, modifiers):
+            print('Mouse button pressed in game')
+            return True
+
+    game_handlers = GameEventHandler()
+
+    def start_game()
+        window.push_handlers(game_handlers)
+
+    def stop_game()
+        window.pop_handlers()
+
+.. note::
+
+    In order to prevent issues with garbage collection, the
+    :py:class:`~pyglet.event.EventDispatcher` class only holds weak
+    references to pushed event handlers. That means the following example
+    will not work, because the pushed object will fall out of scope and be
+    collected::
+
+        dispatcher.push_handlers(MyHandlerClass())
+
+    Instead, you must make sure to keep a reference to the object before pushing
+    it. For example::
+
+        my_handler_instance = MyHandlerClass()
+        dispatcher.push_handlers(my_handler_instance)
+
+Creating your own event dispatcher
+----------------------------------
+
+pyglet provides the :py:class:`~pyglet.window.Window`,
+:py:class:`~pyglet.media.player.Player`, and other event dispatchers,
+but exposes a public interface for creating and dispatching your own events.
+
+The steps for creating an event dispatcher are:
+
+1. Subclass :py:class:`~pyglet.event.EventDispatcher`
+2. Call the :py:meth:`~pyglet.event.EventDispatcher. register_event_type`
+   class method on your subclass for each event your subclass will recognise.
+3. Call :py:meth:`~pyglet.event.EventDispatcher. dispatch_event` to create and
+   dispatch an event as needed.
+
+In the following example, a hypothetical GUI widget provides several events::
+
+    class ClankingWidget(pyglet.event.EventDispatcher):
+        def clank(self):
+            self.dispatch_event('on_clank')
+
+        def click(self, clicks):
+            self.dispatch_event('on_clicked', clicks)
+
+        def on_clank(self):
+            print('Default clank handler.')
+
+    ClankingWidget.register_event_type('on_clank')
+    ClankingWidget.register_event_type('on_clicked')
+
+Event handlers can then be attached as described in the preceding sections::
+
+    widget = ClankingWidget()
+
+    @widget.event
+    def on_clank():
+        pass
+
+    @widget.event
+    def on_clicked(clicks):
+        pass
+
+    def override_on_clicked(clicks):
+        pass
+
+    widget.push_handlers(on_clicked=override_on_clicked)
+
+The :py:class:`~pyglet.event.EventDispatcher` takes care of propagating the
+event to all attached handlers or ignoring it if there are no handlers for
+that event.
+
+There is zero instance overhead on objects that have no event handlers
+attached (the event stack is created only when required).  This makes
+:py:class:`~pyglet.event.EventDispatcher` suitable for use even on light-weight
+objects that may not always have handlers.  For example,
+:py:class:`~pyglet.media.player.Player` is an
+:py:class:`~pyglet.event.EventDispatcher` even though potentially hundreds
+of these objects may be created and destroyed each second, and most will
+not need an event handler.
+
+Implementing the Observer pattern
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The `Observer design pattern`_, also known as Publisher/Subscriber, is a
+simple way to decouple software components.  It is used extensively in many
+large software projects; for example, Java's AWT and Swing GUI toolkits and the
+Python ``logging`` module; and is fundamental to any Model-View-Controller
+architecture.
+
+:py:class:`~pyglet.event.EventDispatcher` can be used to easily add
+observerable components to your application.  The following example recreates
+the `ClockTimer` example from `Design Patterns` (pages 300-301), though
+without needing the bulky ``Attach``, ``Detach`` and ``Notify`` methods::
+
+    # The subject
+    class ClockTimer(pyglet.event.EventDispatcher):
+        def tick(self):
+            self.dispatch_event('on_update')
+    ClockTimer.register_event_type('on_update')
+
+    # Abstract observer class
+    class Observer:
+        def __init__(self, subject):
+            subject.push_handlers(self)
+
+    # Concrete observer
+    class DigitalClock(Observer):
+        def on_update(self):
+            pass
+
+    # Concrete observer
+    class AnalogClock(Observer):
+        def on_update(self):
+            pass
+
+    timer = ClockTimer()
+    digital_clock = DigitalClock(timer)
+    analog_clock = AnalogClock(timer)
+
+The two clock objects will be notified whenever the timer is "ticked", though
+neither the timer nor the clocks needed prior knowledge of the other.  During
+object construction any relationships between subjects and observers can be
+created.
+
+.. _Observer design pattern: Gamma, et al., `Design Patterns` Addison-Wesley 1994
diff --git a/doc/programming_guide/examplegame.rst b/doc/programming_guide/examplegame.rst
new file mode 100644
index 0000000..fbae4b3
--- /dev/null
+++ b/doc/programming_guide/examplegame.rst
@@ -0,0 +1,1156 @@
+.. _programming-guide-game:
+
+In-depth game example
+=====================
+
+This tutorial will walk you through the steps of writing a simple Asteroids
+clone. It is assumed that the reader is familiar with writing and running
+Python programs. This is not a programming tutorial, but it should hopefully
+be clear enough to follow even if you're a beginner. If you get stuck,
+first have a look at the relevant sections of the programming guide.
+The full source code can also be found in the `examples/game/` folder
+of the pyglet source directory, which you can follow along with.
+If anything is still not clear, let us know!
+
+Basic graphics
+--------------
+
+Lets begin!  The first version of our game will simply show a score of zero,
+a label showing the name of the program, three randomly placed asteroids,
+and the player’s ship. Nothing will move.
+
+Setting up
+^^^^^^^^^^
+
+First things first, make sure you have pyglet installed. Then, we will set
+up the folder structure for our project.  Since this example game is written
+in stages, we will have several `version` folders at various stages of
+development.  We will also have a shared resource folder with the images,
+called ‘resources,’ outside of the example folders.  Each `version` folder
+contains a Python file called `asteroid.py` which runs the game, as well as
+a sub-folder named `game` where we will place additional modules; this is
+where most of the logic will be. Your folder structure should look like this::
+
+    game/
+        resources/
+            (images go here)
+        version1/
+            asteroid.py
+            game/
+                __init__.py
+
+Getting a window
+^^^^^^^^^^^^^^^^
+
+To set up a window, simply `import pyglet`, create a new instance of
+:class:`pyglet.window.Window`, and call `pyglet.app.run()`::
+
+    import pyglet
+    game_window = pyglet.window.Window(800, 600)
+
+    if __name__ == '__main__':
+        pyglet.app.run()
+
+If you run the code above, you should see a window full of junk that
+goes away when you press Esc. (What you are seeing is raw uninitialized
+graphics memory).
+
+Loading and displaying an image
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Since our images will reside in a directory other than the example’s root
+directory, we need to tell pyglet where to find them::
+
+    import pyglet
+    pyglet.resource.path = ['../resources']
+    pyglet.resource.reindex()
+
+pyglet's :mod:`pyglet.resource` module takes all of the hard work out of
+finding and loading game resources such as images, sounds, etc..  All that
+you need to do is tell it where to look, and reindex it. In this example
+game, the resource path starts with `../` because the resources folder is
+on the same level as the `version1` folder.  If we left it off, pyglet
+would look inside `version1/` for the `resources/` folder.
+
+Now that pyglet’s resource module is initialized, we can easily load the images
+with the :func:`~pyglet.resource.image` function of the resource module::
+
+    player_image = pyglet.resource.image("player.png")
+    bullet_image = pyglet.resource.image("bullet.png")
+    asteroid_image = pyglet.resource.image("asteroid.png")
+
+Centering the images
+^^^^^^^^^^^^^^^^^^^^
+
+Pyglet will draw and position all images from their lower left corner by
+default.  We don’t want this behavior for our images, which need to rotate
+around their centers.  All we have to do to achieve this is to set their
+anchor points.  Lets create a function to simplify this::
+
+    def center_image(image):
+        """Sets an image's anchor point to its center"""
+        image.anchor_x = image.width // 2
+        image.anchor_y = image.height // 2
+
+Now we can just call center_image() on all of our loaded images::
+
+    center_image(player_image)
+    center_image(bullet_image)
+    center_image(asteroid_image)
+
+Remember that the center_image() function must be defined before it can be
+called at the module level.  Also, note that zero degrees points directly
+to the right in pyglet, so the images are all drawn with their front
+pointing to the right.
+
+To access the images from asteroid.py, we need to use something like
+`from game import resources`, which we’ll get into in the next section.
+
+Initializing objects
+^^^^^^^^^^^^^^^^^^^^
+
+We want to put some labels at the top of the window to give the player some
+information about the score and the current difficulty level.  Eventually,
+we will have a score display, the name of the level, and a row of icons
+representing the number of remaining lives.
+
+Making the labels
+^^^^^^^^^^^^^^^^^
+
+To make a text label in pyglet, just initialize a :class:`pyglet.text.Label` object::
+
+    score_label = pyglet.text.Label(text="Score: 0", x=10, y=460)
+    level_label = pyglet.text.Label(text="My Amazing Game",
+                                x=game_window.width//2, y=game_window.height//2, anchor_x='center')
+
+Notice that the second label is centered using the anchor_x attribute.
+
+Drawing the labels
+^^^^^^^^^^^^^^^^^^
+
+We want pyglet to run some specific code whenever the window is drawn.
+An :meth:`~pyglet.window.Window.on_draw` event is dispatched to the window
+to give it a chance to redraw its contents.  pyglet provides several ways
+to attach event handlers to objects; a simple way is to use a decorator::
+
+    @game_window.event
+    def on_draw():
+        # draw things here
+
+The `@game_window.event` decorator lets the Window instance know that our
+`on_draw()` function is an event handler.
+The :meth:`~pyglet.window.Window.on_draw` event is fired whenever
+- you guessed it - the window needs to be redrawn.  Other events include
+:meth:`~pyglet.window.Window.on_mouse_press` and
+:meth:`~pyglet.window.Window.on_key_press`.
+
+Now we can fill the method with the functions necessary to draw our labels.
+Before we draw anything, we should clear the screen.  After that, we can
+simply call each object’s draw() function::
+
+    @game_window.event
+    def on_draw():
+        game_window.clear()
+
+        level_label.draw()
+        score_label.draw()
+
+Now when you run asteroid.py, you should get a window with a score of zero
+in the upper left corner and a centered label reading “My Amazing Game”
+at the top of the screen.
+
+Making the player and asteroid sprites
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The player should be an instance or subclass of :class:`pyglet.sprite.Sprite`,
+like so::
+
+    from game import resources
+    ...
+    player_ship = pyglet.sprite.Sprite(img=resources.player_image, x=400, y=300)
+
+To get the player to draw on the screen, add a line to `on_draw()`::
+
+    @game_window.event
+    def on_draw():
+        ...
+        player_ship.draw()
+
+Loading the asteroids is a little more complicated, since we’ll need to place
+more than one at random locations that don’t immediately collide with the
+player.  Let’s put the loading code in a new game submodule called load.py::
+
+    import pyglet
+    import random
+    from . import resources
+
+    def asteroids(num_asteroids):
+        asteroids = []
+        for i in range(num_asteroids):
+            asteroid_x = random.randint(0, 800)
+            asteroid_y = random.randint(0, 600)
+            new_asteroid = pyglet.sprite.Sprite(img=resources.asteroid_image,
+                                                x=asteroid_x, y=asteroid_y)
+            new_asteroid.rotation = random.randint(0, 360)
+            asteroids.append(new_asteroid)
+        return asteroids
+
+All we are doing here is making a few new sprites with random positions.
+There’s still a problem, though - an asteroid might randomly be placed
+exactly where the player is, causing immediate death. To fix this issue,
+we’ll need to be able to tell how far away new asteroids are from the player.
+Here is a simple function to calculate that distance::
+
+    import math
+    ...
+    def distance(point_1=(0, 0), point_2=(0, 0)):
+        """Returns the distance between two points"""
+        return math.sqrt((point_1[0] - point_2[0]) ** 2 + (point_1[1] - point_2[1]) ** 2)
+
+To check new asteroids against the player’s position, we need to pass the
+player’s position into the `asteroids()` function and keep regenerating
+new coordinates until the asteroid is far enough away.  pyglet sprites
+keep track of their position both as a tuple (Sprite.position) and as
+x and y attributes (Sprite.x and Sprite.y).  To keep our code short,
+we’ll just pass the position tuple into the function::
+
+    def asteroids(num_asteroids, player_position):
+        asteroids = []
+        for i in range(num_asteroids):
+            asteroid_x, asteroid_y = player_position
+            while distance((asteroid_x, asteroid_y), player_position) < 100:
+                asteroid_x = random.randint(0, 800)
+                asteroid_y = random.randint(0, 600)
+            new_asteroid = pyglet.sprite.Sprite(
+                img=resources.asteroid_image, x=asteroid_x, y=asteroid_y)
+            new_asteroid.rotation = random.randint(0, 360)
+            asteroids.append(new_asteroid)
+        return asteroids
+
+For each asteroid, it chooses random positions until it finds one away from
+the player, creates the sprite, and gives it a random rotation. Each asteroid
+is appended to a list, which is returned.
+
+Now you can load three asteroids like this::
+
+    from game import resources, load
+    ...
+    asteroids = load.asteroids(3, player_ship.position)
+
+The asteroids variable now contains a list of sprites. Drawing them on the
+screen is as simple as it was for the player’s ship - just call their
+:meth:`~pyglet.sprite.Sprite.draw` methods:
+
+.. code:: python
+
+    @game_window.event
+    def on_draw():
+        ...
+        for asteroid in asteroids:
+            asteroid.draw()
+
+This wraps up the first section.  Your "game" doesn't do much of anything yet,
+but we'll get to that in the following sections.  You may want to look over
+the `examples/game/version1` folder in the pyglet source to review what we've
+done, and to find a functional copy.
+
+
+Basic motion
+------------
+
+In the second version of the example, we’ll introduce a simpler, faster way
+to draw all of the game objects, as well as add row of icons indicating the
+number of lives left.  We’ll also write some code to make the player and the
+asteroids obey the laws of physics.
+
+Drawing with batches
+^^^^^^^^^^^^^^^^^^^^
+
+Calling each object’s `draw()` method manually can become cumbersome and
+tedious if there are many different kinds of objects.  It's also very
+inefficient if you need to draw a large number of objects. The pyglet
+:class:`pyglet.graphics.Batch` class simplifies drawing by letting you draw
+all your objects with a single function call.  All you need to do is create
+a batch, pass it into each object you want to draw, and call the batch’s
+:meth:`~pyglet.graphics.Batch.draw` method.
+
+To create a new batch, simply create an instance of :class:`pyglet.graphics.Batch`::
+
+    main_batch = pyglet.graphics.Batch()
+
+To make an object a member of a batch, just pass the batch into its
+constructor as the batch keyword argument::
+
+    score_label = pyglet.text.Label(text="Score: 0", x=10, y=575, batch=main_batch)
+
+Add the batch keyword argument to each graphical object created in asteroid.py.
+
+To use the batch with the asteroid sprites, we’ll need to pass the batch into
+the `game.load.asteroid()` function, then just add it as a keyword argument to
+each new sprite. Update the function::
+
+    def asteroids(num_asteroids, player_position, batch=None):
+        ...
+        new_asteroid = pyglet.sprite.Sprite(img=resources.asteroid_image,
+                                            x=asteroid_x, y=asteroid_y,
+                                            batch=batch)
+
+And update the place where it’s called::
+
+    asteroids = load.asteroids(3, player_ship.position, main_batch)
+
+Now you can replace those five lines of `draw()` calls with just one::
+
+    main_batch.draw()
+
+Now when you run asteroid.py, it should look exactly the same.
+
+Displaying little ship icons
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To show how many lives the player has left, we’ll need to draw a little row
+of icons in the upper right corner of the screen.  Since we’ll be making more
+than one using the same template, let’s create a function called
+`player_lives()` in the `load` module to generate them. The icons should look
+the same as the player’s ship.  We could create a scaled version using an
+image editor, or we could just let pyglet do the scaling.  I don’t know about
+you, but I prefer the option that requires less work.
+
+The function for creating the icons is almost exactly the same as the one for
+creating asteroids. For each icon we just create a sprite, give it a position
+and scale, and append it to the return list::
+
+    def player_lives(num_icons, batch=None):
+        player_lives = []
+        for i in range(num_icons):
+            new_sprite = pyglet.sprite.Sprite(img=resources.player_image,
+                                              x=785-i*30, y=585, batch=batch)
+            new_sprite.scale = 0.5
+            player_lives.append(new_sprite)
+        return player_lives
+
+The player icon is 50x50 pixels, so half that size will be 25x25.  We want to
+put a little bit of space between each icon, so we create them at 30-pixel
+intervals starting from the right side of the screen and moving to the left.
+Note that like the `asteroids()` function, `player_lives()` takes a `batch`
+argument.
+
+Making things move
+^^^^^^^^^^^^^^^^^^
+
+The game would be pretty boring if nothing on the screen ever moved. To
+achieve motion, we’ll need to write our own set of classes to handle
+frame-by-frame movement calculations.  We’ll also need to write a Player
+class to respond to keyboard input.
+
+**Creating the basic motion class**
+
+Since every visible object is represented by at least one Sprite, we may as
+well make our basic motion class a subclass of pyglet.sprite.Sprite. Another
+approach would be to have our class have a sprite attribute.
+
+Create a new game submodule called physicalobject.py and declare a
+PhysicalObject class. The only new attributes we’ll be adding will store the
+object’s velocity, so the constructor will be simple::
+
+    class PhysicalObject(pyglet.sprite.Sprite):
+
+        def __init__(self, *args, **kwargs):
+            super().__init__(*args, **kwargs)
+
+            self.velocity_x, self.velocity_y = 0.0, 0.0
+
+Each object will need to be updated every frame, so let’s write an `update()`
+method::
+
+    def update(self, dt):
+        self.x += self.velocity_x * dt
+        self.y += self.velocity_y * dt
+
+What’s dt?  It’s the "delta time", or "time step".  Game frames are not
+instantaneous, and they don’t always take equal amounts of time to draw.
+If you’ve ever tried to play a modern game on an old machine, you know
+that frame rates can jump all over the place.  There are a number of
+ways to deal with this problem, the simplest one being to just multiply all
+time-sensitive operations by dt. I’ll show you how this value is calculated
+later.
+
+If we give objects a velocity and just let them go, they will fly off the
+screen before long. Since we’re making an Asteroids clone, we would rather
+they just wrapped around the screen. Here is a simple function that
+accomplishes the goal::
+
+    def check_bounds(self):
+        min_x = -self.image.width / 2
+        min_y = -self.image.height / 2
+        max_x = 800 + self.image.width / 2
+        max_y = 600 + self.image.height / 2
+        if self.x < min_x:
+            self.x = max_x
+        elif self.x > max_x:
+            self.x = min_x
+        if self.y < min_y:
+            self.y = max_y
+        elif self.y > max_y:
+            self.y = min_y
+
+As you can see, it simply checks to see if objects are no longer visible on
+the screen, and if so, it moves them to the other side of the screen.
+To make every PhysicalObject use this behavior, add a call to
+`self.check_bounds()` at the end of `update()`.
+
+To make the asteroids use our new motion code, just import the physicalobject
+module and change the `new_asteroid = ...` line to create a new
+`PhysicalObject` instead of a `Sprite`.  You’ll also want to give them a random
+initial velocity.  Here is the new, improved `load.asteroids()` function:
+
+.. code:: python
+
+    def asteroids(num_asteroids, player_position, batch=None):
+        ...
+        new_asteroid = physicalobject.PhysicalObject(...)
+        new_asteroid.rotation = random.randint(0, 360)
+        new_asteroid.velocity_x = random.random()*40
+        new_asteroid.velocity_y = random.random()*40
+        ...
+
+Writing the game update function
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To call each object’s `update()` method every frame, we first need to have a
+list of those objects. For now, we can just declare it after setting up all
+the other objects::
+
+    game_objects = [player_ship] + asteroids
+
+Now we can write a simple function to iterate over the list::
+
+    def update(dt):
+        for obj in game_objects:
+            obj.update(dt)
+
+The `update()` function takes a `dt` parameter because it is still not the
+source of the actual time step.
+
+Calling the update() function
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+We need to update the objects at least once per frame.  What’s a frame?  Well,
+most screens have a maximum refresh rate of 60 hertz.  If we set our loop to
+run at exactly 60 hertz, though, the motion will look a little jerky because
+it won’t match the screen exactly.  Instead, we can have it
+update twice as fast, 120 times per second, to get smooth animation.
+
+The best way to call a function 120 times per second is to ask pyglet to do it.
+The :mod:`pyglet.clock` module contains a number of ways to call functions
+periodically or at some specified time in the future.  The one we want is
+:meth:`pyglet.clock.schedule_interval`::
+
+    pyglet.clock.schedule_interval(update, 1/120.0)
+
+Putting this line above `pyglet.app.run()` in the if `__name__ == '__main__'`
+block tells pyglet to call `update()` 120 times per second.  Pyglet will pass
+in the elapsed time, i.e. `dt`, as the only parameter.
+
+Now when you run asteroid.py, you should see your formerly static asteroids
+drifting serenely across the screen, reappearing on the other side when they
+slide off the edge.
+
+Writing the Player class
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+In addition to obeying the basic laws of physics, the player object needs to
+respond to keyboard input.  Start by creating a `game.player` module,
+importing the appropriate modules, and subclassing `PhysicalObject`::
+
+    from . import physicalobject, resources
+
+
+    class Player(physicalobject.PhysicalObject):
+
+        def __init__(self, *args, **kwargs):
+            super().__init__(img=resources.player_image, *args, **kwargs)
+
+So far, the only difference between a Player and a PhysicalObject is that a
+Player will always have the same image.  But Player objects need a couple
+more attributes.  Since the ship will always thrust with the same force in
+whatever direction it points, we’ll need to define a constant for the
+magnitude of that force.  We should also define a constant for the ship’s
+rotation speed::
+
+        self.thrust = 300.0
+        self.rotate_speed = 200.0
+
+Now we need to get the class to respond to user input.  Pyglet uses an
+event-based approach to input, sending key press and key release events
+to registered event handlers.  But we want to use a polling approach in
+this example, checking periodically if a key is down.  One way to accomplish
+that is to maintain a dictionary of keys.  First, we need to initialize the
+dictionary in the constructor::
+
+        self.keys = dict(left=False, right=False, up=False)
+
+Then we need to write two methods, `on_key_press()` and `on_key_release()`.
+When pyglet checks a new event handler, it looks for these two methods,
+among others::
+
+    import math
+    from pyglet.window import key
+    from . import physicalobject, resources
+
+    class Player(physicalobject.PhysicalObject)
+
+        def on_key_press(self, symbol, modifiers):
+            if symbol == key.UP:
+                self.keys['up'] = True
+            elif symbol == key.LEFT:
+                self.keys['left'] = True
+            elif symbol == key.RIGHT:
+                self.keys['right'] = True
+
+        def on_key_release(self, symbol, modifiers):
+            if symbol == key.UP:
+                self.keys['up'] = False
+            elif symbol == key.LEFT:
+                self.keys['left'] = False
+            elif symbol == key.RIGHT:
+                self.keys['right'] = False
+
+That looks pretty cumbersome. There’s a better way to do it which we’ll see
+later, but for now, this version serves as a good demonstration of pyglet’s
+event system.
+
+The last thing we need to do is write the `update()` method.  It follows the
+same behavior as a PhysicalObject plus a little extra, so we’ll need to call
+PhysicalObject's `update()` method and then respond to input::
+
+    def update(self, dt):
+        super(Player, self).update(dt)
+
+        if self.keys['left']:
+            self.rotation -= self.rotate_speed * dt
+        if self.keys['right']:
+            self.rotation += self.rotate_speed * dt
+
+Pretty simple so far.  To rotate the player, we just add the rotation speed
+to the angle, multiplied by dt to account for time.  Note that Sprite objects’
+rotation attributes are in degrees, with clockwise as the positive direction.
+This means that you need to call `math.degrees()` or `math.radians()` and make
+the result negative whenever you use Python’s built-in math functions with
+the Sprite class, since those functions use radians instead of degrees, and
+their positive direction is counter-clockwise.  The code to make the ship
+thrust forward uses an example of such a conversion::
+
+        if self.keys['up']:
+            angle_radians = -math.radians(self.rotation)
+            force_x = math.cos(angle_radians) * self.thrust * dt
+            force_y = math.sin(angle_radians) * self.thrust * dt
+            self.velocity_x += force_x
+            self.velocity_y += force_y
+
+First, we convert the angle to radians so that `math.cos()` and `math.sin()`
+will get the correct values.  Then we apply some simple physics to modify the
+ship’s X and Y velocity components and push the ship in the right direction.
+
+We now have a complete Player class.  If we add it to the game and tell pyglet
+that it’s an event handler, we should be good to go.
+
+Integrating the player class
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The first thing we need to do is make player_ship an instance of Player::
+
+    from game import player
+    ...
+    player_ship = player.Player(x=400, y=300, batch=main_batch)
+
+Now we need to tell pyglet that player_ship is an event handler.  To do that,
+we need to push it onto the event stack with `game_window.push_handlers()`::
+
+    game_window.push_handlers(player_ship)
+
+That’s it! Now you should be able to run the game and move the player with the
+arrow keys.
+
+
+Giving the player something to do
+---------------------------------
+
+In any good game, there needs to be something working against the player.
+In the case of Asteroids, it’s the threat of collision with, well, an asteroid.
+Collision detection requires a lot of infrastructure in the code, so this
+section will focus on making it work.  We’ll also clean up the
+player class and show some visual feedback for thrusting.
+
+Simplifying player input
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+Right now, the Player class handles all of its own keyboard events.
+It spends 13 lines of code doing nothing but setting boolean values in a
+dictionary.  One would think that there would be a better way, and there is:
+:class:`pyglet.window.key.KeyStateHandler`.  This handy class automatically
+does what we have been doing manually: it tracks the state of every key on the
+keyboard.
+
+To start using it, we need to initialize it and push it onto the event stack
+instead of the Player class.  First, let’s add it to Player‘s constructor::
+
+    self.key_handler = key.KeyStateHandler()
+
+We also need to push the key_handler object onto the event stack.  Keep pushing
+the player_ship object in addition to its key handler, because we’ll need it
+to keep handling key press and release events later::
+
+    game_window.push_handlers(player_ship.key_handler)
+
+Since Player now relies on key_handler to read the keyboard, we need to change
+the `update()` method to use it.  The only changes are in the if conditions::
+
+    if self.key_handler[key.LEFT]:
+        ...
+    if self.key_handler[key.RIGHT]:
+        ...
+    if self.key_handler[key.UP]:
+        ...
+
+Now we can remove the `on_key_press()` and `on_key_release()` methods
+from the class. It’s just that simple.  If you need to see a list of key
+constants, you can check the API documentation under
+:class:`pyglet.window.key`.
+
+Adding an engine flame
+^^^^^^^^^^^^^^^^^^^^^^
+
+Without visual feedback, it can be difficult to tell if the ship is actually
+thrusting forward or not, especially for an observer just watching someone
+else play the game.  One way to provide visual feedback is to show an engine
+flame behind the player while the player is thrusting.
+
+Loading the flame image
+^^^^^^^^^^^^^^^^^^^^^^^
+
+The player will now be made of two sprites.  There’s nothing preventing us
+from letting a Sprite own another Sprite, so we’ll just give Player an
+engine_sprite attribute and update it every frame. For our purposes,
+this approach will be the simplest and most scalable.
+
+To make the flame draw in the correct position, we could either do some
+complicated math every frame, or we could just move the image’s anchor point.
+First, load the image in resources.py::
+
+    engine_image = pyglet.resource.image("engine_flame.png")
+
+To get the flame to draw behind the player, we need to move the flame image’s
+center of rotation to the right, past the end of the image.
+To do that, we just set its `anchor_x` and `anchor_y` attributes::
+
+    engine_image.anchor_x = engine_image.width * 1.5
+    engine_image.anchor_y = engine_image.height / 2
+
+Now the image is ready to be used by the player class.  If you’re still
+confused about anchor points, experiment with the values for engine_image’s
+anchor point when you finish this section.
+
+Creating and drawing the flame
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The engine sprite needs to be initialized with all the same arguments as
+Player, except that it needs a different image and must be initially invisible.
+The code for creating it belongs in `Player.__init__()` and is very
+straightforward::
+
+    self.engine_sprite = pyglet.sprite.Sprite(img=resources.engine_image, *args, **kwargs)
+    self.engine_sprite.visible = False
+
+To make the engine sprite appear only while the player is thrusting, we need
+to add some logic to the if `self.key_handler[key.UP]` block in the `update()`
+method::
+
+    if self.key_handler[key.UP]:
+        ...
+        self.engine_sprite.visible = True
+    else:
+        self.engine_sprite.visible = False
+
+To make the sprite appear at the player’s position, we also need to update
+its position and rotation attributes::
+
+    if self.key_handler[key.UP]:
+        ...
+        self.engine_sprite.rotation = self.rotation
+        self.engine_sprite.x = self.x
+        self.engine_sprite.y = self.y
+        self.engine_sprite.visible = True
+    else:
+        self.engine_sprite.visible = False
+
+Cleaning up after death
+^^^^^^^^^^^^^^^^^^^^^^^
+
+When the player is inevitably smashed to bits by an asteroid, he will
+disappear from the screen. However, simply removing the Player instance
+from the game_objects list is not enough for it to be removed from the
+graphics batch.  To do that, we need to call its `delete()` method.
+Normally a Sprite‘s own `delete()` method will work fine without modifications,
+but our subclass has its own child Sprite (the engine flame) which must
+also be deleted when the Player instance is deleted. To get both to die
+gracefully, we must write a simple but slightly enhanced `delete()` method::
+
+    def delete(self):
+        self.engine_sprite.delete()
+        super(Player, self).delete()
+
+The Player class is now cleaned up and ready to go.
+
+Checking For collisions
+^^^^^^^^^^^^^^^^^^^^^^^
+
+To make objects disappear from the screen, we’ll need to manipulate the game
+objects list. Every object will need to check every other object’s position
+against its own, and each object will have to decide whether or not it should
+be removed from the list.  The game loop will then check for dead objects
+and remove them from the list.
+
+Checking all object pairs
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+We need to check every object against every other object.  The simplest
+method is to use nested loops.  This method will be inefficient for a large
+number of objects, but it will work for our purposes.  We can use one easy
+optimization and avoid checking the same pair of objects twice.
+Here’s the setup for the loops, which belongs in `update()`.
+It simply iterates over all object pairs without doing anything::
+
+    for i in range(len(game_objects)):
+        for j in range(i+1, len(game_objects)):
+            obj_1 = game_objects[i]
+            obj_2 = game_objects[j]
+
+We’ll need a way to check if an object has already been killed.  We could go
+over to PhysicalObject right now and put it in, but let’s keep working on
+the game loop and implement the method later. For now, we’ll just assume that
+everything in game_objects has a dead attribute which will be False
+until the class sets it to True, at which point it will be ignored and
+eventually removed from the list.
+
+To perform the actual check, we’ll also need to call two more methods that
+don’t exist yet. One method will determine if the two objects actually collide,
+and the other method will give each object an opportunity to respond to
+the collision.  The checking code itself is easy to understand,
+so I won’t bother you with further explanations::
+
+        if not obj_1.dead and not obj_2.dead:
+            if obj_1.collides_with(obj_2):
+                obj_1.handle_collision_with(obj_2)
+                obj_2.handle_collision_with(obj_1)
+
+Now all that remains is for us to go through the list and remove dead objects::
+
+    for to_remove in [obj for obj in game_objects if obj.dead]:
+        to_remove.delete()
+        game_objects.remove(to_remove)
+
+As you can see, it simply calls the object’s `delete()` method to remove it
+from any batches, then it removes it from the list.  If you haven’t used list
+comprehensions much, the above code might look like it’s removing objects
+from the list while traversing it.  Fortunately, the list comprehension is
+evaluated before the loop actually runs, so there should be no problems.
+
+Implementing the collision functions
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+We need to add three things to the PhysicalObject class: the dead attribute,
+the `collides_with()` method, and the `handle_collision_with()` method.
+The `collides_with()` method will need to use the `distance()` function,
+so let’s start by moving that function into its own submodule of game,
+called util.py::
+
+    import pyglet, math
+
+    def distance(point_1=(0, 0), point_2=(0, 0)):
+        return math.sqrt(
+            (point_1[0] - point_2[0]) ** 2 +
+            (point_1[1] - point_2[1]) ** 2)
+
+Remember to call from util import distance in load.py.  Now we can write
+`PhysicalObject.collides_with()` without duplicating code::
+
+    def collides_with(self, other_object):
+        collision_distance = self.image.width/2 + other_object.image.width/2
+        actual_distance = util.distance(self.position, other_object.position)
+
+        return (actual_distance <= collision_distance)
+
+The collision handler function is even simpler, since for now we just want
+every object to die as soon as it touches another object::
+
+    def handle_collision_with(self, other_object):
+        self.dead = True
+
+One last thing: set self.dead = False in PhysicalObject.__init__().
+
+And that’s it! You should be able to zip around the screen, engine blazing
+away.  If you hit something, both you and the thing you collided with should
+disappear from the screen.  There’s still no game, but we are clearly
+making progress.
+
+
+Collision response
+------------------
+
+In this section, we’ll add bullets.  This new feature will require us to
+start adding things to the game_objects list during the game,
+as well as have objects check each others’ types to make a decision about
+whether or not they should die.
+
+Adding objects during play
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+**How?**
+
+We handled object removal with a boolean flag.  Adding objects will be
+a little bit more complicated.  For one thing, an object can’t just say
+“Add me to the list!”  It has to come from somewhere.
+For another thing, an object might want to add more than one other
+object at a time.
+
+There are a few ways to solve this problem.  To avoid circular references,
+keep our constructors nice and short, and avoid adding extra modules,
+we’ll have each object keep a list of new child objects to be added to
+game_objects.  This approach will make it easy for any object in the game
+to spawn more objects.
+
+Tweaking the game loop
+^^^^^^^^^^^^^^^^^^^^^^
+
+The simplest way to check objects for children and add those children to
+the list is to add two lines of code to the game_objects loop.
+We haven’t implemented the new_objects attribute yet, but when we do,
+it will be a list of objects to add::
+
+    for obj in game_objects:
+        obj.update(dt)
+        game_objects.extend(obj.new_objects)
+        obj.new_objects = []
+
+Unfortunately, this simple solution is problematic.  It’s generally a
+bad idea to modify a list while iterating over it.  The fix is to simply
+add new objects to a separate list, then add the objects in the separate
+list to game_objects after we have finished iterating over it.
+
+Declare a to_add list just above the loop and add new objects to it instead.
+At the very bottom of `update()`, after the object removal code,
+add the objects in to_add to game_objects::
+
+    ...collision...
+
+    to_add = []
+
+    for obj in game_objects:
+        obj.update(dt)
+        to_add.extend(obj.new_objects)
+        obj.new_objects = []
+
+    ...removal...
+
+    game_objects.extend(to_add)
+
+Putting the attribute in PhysicalObject
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+As mentioned before, all we have to do is declare a new_objects attribute
+in the PhysicalObject class::
+
+    def __init__(self, *args, **kwargs):
+        ....
+        self.new_objects = []
+
+To add a new object, all we have to do is put something in new_objects,
+and the main loop will see it, add it to the game_objects list,
+and clear new_objects.
+
+Adding bullets
+^^^^^^^^^^^^^^
+
+**Writing the bullet class**
+
+For the most part, bullets act like any other PhysicalObject, but they have
+two differences, at least in this game: they only collide with some objects,
+and they disappear from the screen after a couple of seconds to prevent the
+player from flooding the screen with bullets.
+
+First, make a new submodule of game called bullet.py and start a simple
+subclass of PhysicalObject::
+
+    import pyglet
+    from . import physicalobject, resources
+
+    class Bullet(physicalobject.PhysicalObject):
+        """Bullets fired by the player"""
+
+        def __init__(self, *args, **kwargs):
+            super(Bullet, self).__init__(
+                resources.bullet_image, *args, **kwargs)
+
+To get bullets to disappear after a time, we could keep track of our own
+age and lifespan attributes, or we could let pyglet do all the work for us.
+I don’t know about you, but I prefer the second option.
+First, we need to write a function to call at the end of a bullet’s life::
+
+    def die(self, dt):
+        self.dead = True
+
+Now we need to tell pyglet to call it after half a second or so.
+We can do this as soon as the object is initialized by adding a call to
+:meth:`pyglet.clock.schedule_once` to the constructor::
+
+    def __init__(self, *args, **kwargs):
+        super(Bullet, self).__init__(resources.bullet_image, *args, **kwargs)
+        pyglet.clock.schedule_once(self.die, 0.5)
+
+There’s still more work to be done on the Bullet class, but before we
+do any more work on the class itself, let’s get them on the screen.
+
+Firing bullets
+^^^^^^^^^^^^^^
+
+The Player class will be the only class that fires bullets,
+so let’s open it up, import the bullet module, and add a bullet_speed attribute
+to its constructor::
+
+    ...
+    from . import bullet
+
+    class Player(physicalobject.PhysicalObject):
+        def __init__(self, *args, **kwargs):
+            super(Player, self).__init__(img=resources.player_image, *args, **kwargs)
+            ...
+            self.bullet_speed = 700.0
+
+Now we can write the code to create a new bullet and send it hurling off
+into space.  First, we need to resurrect the on_key_press() event handler::
+
+    def on_key_press(self, symbol, modifiers):
+        if symbol == key.SPACE:
+            self.fire()
+
+The `fire()` method itself will be a bit more complicated.  Most of the
+calculations will be very similar to the ones for thrusting, but there
+will be some differences.  We’ll need to spawn the bullet out at the
+nose of the ship, not at its center.  We’ll also need to add the ship’s
+existing velocity to the bullet’s new velocity, or the bullets will
+end up going slower than the ship if the player gets going fast enough.
+
+As usual, convert to radians and reverse the direction::
+
+    def fire(self):
+        angle_radians = -math.radians(self.rotation)
+
+Next, calculate the bullet’s position and instantiate it::
+
+    ship_radius = self.image.width/2
+    bullet_x = self.x + math.cos(angle_radians) * ship_radius
+    bullet_y = self.y + math.sin(angle_radians) * ship_radius
+    new_bullet = bullet.Bullet(bullet_x, bullet_y, batch=self.batch)
+
+Set its velocity using almost the same equations::
+
+    bullet_vx = (
+        self.velocity_x +
+        math.cos(angle_radians) * self.bullet_speed
+    )
+    bullet_vy = (
+        self.velocity_y +
+        math.sin(angle_radians) * self.bullet_speed
+    )
+    new_bullet.velocity_x = bullet_vx
+    new_bullet.velocity_y = bullet_vy
+
+Finally, add it to the new_objects list so that the main loop will pick it up
+and add it to game_objects::
+
+    self.new_objects.append(new_bullet)
+
+At this point, you should be able to fire bullets out of the front of your
+ship.  There’s just one problem: as soon as you fire, your ship disappears.
+You may have noticed earlier that asteroids also disappear when they touch
+each other.  To fix this problem, we’ll need to start customizing
+each class’s `handle_collision_with()` method.
+
+Customizing collision behavior
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+There are five kinds of collisions in the current version of the game:
+bullet-asteroid, bullet-player, asteroid-player, bullet-bullet,
+and asteroid-asteroid.  There would be many more in a more complex game.
+
+In general, objects of the same type should not be destroyed when they collide,
+so we can generalize that behavior in PhysicalObject. Other interactions will
+require a little more work.
+
+**Letting twins ignore each other**
+
+To let two asteroids or two bullets pass each other by without a word of
+acknowledgement (or a dramatic explosion), we just need to check if their
+classes are equal in the PhysicalObject.handle_collision_with() method::
+
+    def handle_collision_with(self, other_object):
+        if other_object.__class__ == self.__class__:
+            self.dead = False
+        else:
+            self.dead = True
+
+There are a few other, more elegant ways to check for object equality in
+Python, but the above code gets the job done.
+
+Customizing bullet collisions
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Since bullet collision behavior can vary so wildly across objects, let’s add
+a reacts_to_bullets attribute to PhysicalObjects which the Bullet class can
+check to determine if it should register a collision or not.
+We should also add an is_bullet attribute so we can check the collision
+properly from both objects.
+
+(These are not “good” design decisions, but they will work.)
+
+First, initialize the reacts_to_bullets attribute to True in the
+PhysicalObject constructor::
+
+    class PhysicalObject(pyglet.sprite.Sprite):
+        def __init__(self, *args, **kwargs):
+            ...
+            self.reacts_to_bullets = True
+            self.is_bullet = False
+            ...
+
+    class Bullet(physicalobject.PhysicalObject):
+        def __init__(self, *args, **kwargs):
+            ...
+            self.is_bullet = True
+
+Then, insert a bit of code in `PhysicalObject.collides_with()` to ignore
+bullets under the right circumstances::
+
+    def collides_with(self, other_object):
+        if not self.reacts_to_bullets and other_object.is_bullet:
+            return False
+        if self.is_bullet and not other_object.reacts_to_bullets:
+            return False
+        ...
+
+Finally, set self.reacts_to_bullets = False in Player.__init__().  The `Bullet`
+class is completely finished!  Now let’s make something happen when a bullet
+hits an asteroid.
+
+Making asteroids explode
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+Asteroids is challenging to players because every time you shoot an asteroid,
+it turns into more asteroids.  We need to mimic that behavior if we want our
+game to be any fun.  We’ve already done most of the hard parts.
+All that remains is to make another subclass of PhysicalObject and write
+a custom `handle_collision_with()` method, along with a couple of maintenance
+tweaks.
+
+Writing the asteroid class
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Create a new submodule of game called asteroid.py.  Write the usual constructor
+to pass a specific image to the superclass, passing along any other parameters::
+
+    import pyglet
+    from . import resources, physicalobject
+
+    class Asteroid(physicalobject.PhysicalObject):
+        def __init__(self, *args, **kwargs):
+            super(Asteroid, self).__init__(resources.asteroid_image, *args, **kwargs)
+
+Now we need to write a new `handle_collision_with()` method.  It should create
+a random number of new, smaller asteroids with random velocities.  However,
+it should only do that if it’s big enough. An asteroid should divide at most
+twice, and if we scale it down by half each time, then an asteroid should stop
+dividing when it’s 1/4 the size of a new asteroid.
+
+We want to keep the old behavior of ignoring other asteroids, so start the
+method with a call to the superclass’s method::
+
+    def handle_collision_with(self, other_object):
+        super(Asteroid, self).handle_collision_with(other_object)
+
+Now we can say that if it’s supposed to die, and it’s big enough, then we
+should create two or three new asteroids with random rotations and velocities.
+We should add the old asteroid’s velocity to the new ones to make it look
+like they come from the same object::
+
+    import random
+
+    class Asteroid:
+        def handle_collision_with(self, other_object):
+            super(Asteroid, self).handle_collision_with(other_object)
+            if self.dead and self.scale > 0.25:
+                num_asteroids = random.randint(2, 3)
+                for i in range(num_asteroids):
+                    new_asteroid = Asteroid(x=self.x, y=self.y, batch=self.batch)
+                    new_asteroid.rotation = random.randint(0, 360)
+                    new_asteroid.velocity_x = (random.random() * 70 + self.velocity_x)
+                    new_asteroid.velocity_y = (random.random() * 70 + self.velocity_y)
+                    new_asteroid.scale = self.scale * 0.5
+                    self.new_objects.append(new_asteroid)
+
+While we’re here, let’s add a small graphical touch to the asteroids by making
+them rotate a little.  To do that, we’ll add a rotate_speed attribute and give
+it a random value.  Then we’ll write an `update()` method to apply that
+rotation every frame.
+
+Add the attribute in the constructor::
+
+    def __init__(self, *args, **kwargs):
+        super(Asteroid, self).__init__(resources.asteroid_image, *args, **kwargs)
+        self.rotate_speed = random.random() * 100.0 - 50.0
+
+Then write the update() method::
+
+    def update(self, dt):
+        super(Asteroid, self).update(dt)
+        self.rotation += self.rotate_speed * dt
+
+The last thing we need to do is go over to load.py and have the asteroid()
+method create a new Asteroid instead of a PhysicalObject::
+
+    from . import asteroid
+
+    def asteroids(num_asteroids, player_position, batch=None):
+        ...
+        for i in range(num_asteroids):
+            ...
+            new_asteroid = asteroid.Asteroid(x=asteroid_x, y=asteroid_y, batch=batch)
+            ...
+        return asteroids
+
+Now we’re looking at something resembling a game.  It's simple, but all of
+the basics are there.
+
+
+Next steps
+----------
+
+So instead of walking you through a standard refactoring session,
+I’m going to leave it as an exercise for you to do the following::
+
+* Make the Score counter mean something
+* Let the player restart the level if they die
+* Implement lives and a “Game Over” screen
+* Add particle effects
+
+Good luck!  With a little effort, you should be able to figure out most of
+these things on your own. If you have trouble, join us on the pyglet
+mailing list.
+
+Also, in addition to this example game, there is yet *another* Asteroids clone
+available in the `/examples/astraea/` folder in the pyglet source directory.
+In comparison to this example game excercise we've just completed,
+Astraea is a complete game with a proper menu, score system, and additional
+graphical effects.  No step-by-step documentation is available for Astraea,
+but the code itself should be easy to understand and illustrates some nice
+techniques.
diff --git a/doc/programming_guide/gl.rst b/doc/programming_guide/gl.rst
new file mode 100644
index 0000000..8a969d4
--- /dev/null
+++ b/doc/programming_guide/gl.rst
@@ -0,0 +1,228 @@
+.. _guide_gl:
+
+The OpenGL interface
+====================
+
+pyglet provides an interface to OpenGL and GLU.  The interface is used by all
+of pyglet's higher-level API's, so that all rendering is done efficiently by
+the graphics card, rather than the operating system.  You can access this
+interface directly; using it is much like using OpenGL from C.
+
+The interface is a "thin-wrapper" around ``libGL.so`` on Linux,
+``opengl32.dll`` on Windows and ``OpenGL.framework`` on OS X.  The pyglet
+maintainers regenerate the interface from the latest specifications, so it is
+always up-to-date with the latest version and almost all extensions.
+
+The interface is provided by the ``pyglet.gl`` package.  To use it you will
+need a good knowledge of OpenGL, C and ctypes.  You may prefer to use OpenGL
+without using ctypes, in which case you should investigate `PyOpenGL`_.
+`PyOpenGL`_ provides similar functionality with a more "Pythonic" interface,
+and will work with pyglet without any modification.
+
+.. _PyOpenGL: http://pyopengl.sourceforge.net/
+
+Using OpenGL
+------------
+
+Documentation of OpenGL and GLU are provided at the `OpenGL website`_ and
+(more comprehensively) in the `OpenGL Programming SDK`_.
+
+Importing the package gives access to OpenGL, GLU, and all OpenGL registered
+extensions.   This is sufficient for all but the most advanced uses of
+OpenGL::
+
+    from pyglet.gl import *
+
+All function names and constants are identical to the C counterparts.  For
+example, the following program draws a triangle on the screen::
+
+    from pyglet.gl import *
+
+    # Direct OpenGL commands to this window.
+    window = pyglet.window.Window()
+
+    @window.event
+    def on_draw():
+        glClear(GL_COLOR_BUFFER_BIT)
+        glLoadIdentity()
+        glBegin(GL_TRIANGLES)
+        glVertex2f(0, 0)
+        glVertex2f(window.width, 0)
+        glVertex2f(window.width, window.height)
+        glEnd()
+
+    pyglet.app.run()
+
+Some OpenGL functions require an array of data.  These arrays must be
+constructed as ``ctypes`` arrays of the correct type.  The following example
+draw the same triangle as above, but uses a vertex array instead of the
+immediate-mode functions.  Note the construction of the vertex array using a
+one-dimensional ``ctypes`` array of ``GLfloat``::
+
+    from pyglet.gl import *
+
+    window = pyglet.window.Window()
+
+    vertices = [0, 0,
+                window.width, 0,
+                window.width, window.height]
+    vertices_gl_array = (GLfloat * len(vertices))(*vertices)
+
+    glEnableClientState(GL_VERTEX_ARRAY)
+    glVertexPointer(2, GL_FLOAT, 0, vertices_gl_array)
+
+    @window.event
+    def on_draw():
+        glClear(GL_COLOR_BUFFER_BIT)
+        glLoadIdentity()
+        glDrawArrays(GL_TRIANGLES, 0, len(vertices) // 2)
+
+    pyglet.app.run()
+
+Similar array constructions can be used to create data for vertex buffer
+objects, texture data, polygon stipple data and the map functions.
+
+.. _OpenGL Website: http://www.opengl.org
+.. _OpenGL Programming SDK: http://www.opengl.org/sdk
+
+Resizing the window
+-------------------
+
+pyglet sets up the viewport and an orthographic projection on each window
+automatically.  It does this in a default
+:py:meth:`~pyglet.window.Window.on_resize` handler defined on
+:py:class:`~pyglet.window.Window`::
+
+    @window.event
+    def on_resize(width, height):
+        glViewport(0, 0, width, height)
+        glMatrixMode(gl.GL_PROJECTION)
+        glLoadIdentity()
+        glOrtho(0, width, 0, height, -1, 1)
+        glMatrixMode(gl.GL_MODELVIEW)
+
+If you need to define your own projection (for example, to use
+a 3-dimensional perspective projection), you should override this
+event with your own; for example::
+
+    @window.event
+    def on_resize(width, height):
+        glViewport(0, 0, width, height)
+        glMatrixMode(GL_PROJECTION)
+        glLoadIdentity()
+        gluPerspective(65, width / float(height), .1, 1000)
+        glMatrixMode(GL_MODELVIEW)
+        return pyglet.event.EVENT_HANDLED
+
+Note that the :py:meth:`~pyglet.window.Window.on_resize` handler is called for
+a window the first time it is displayed, as well as any time it is later
+resized.
+
+Error checking
+--------------
+
+By default, pyglet calls ``glGetError`` after every GL function call (except
+where such a check would be invalid).  If an error is reported, pyglet raises
+``GLException`` with the result of ``gluErrorString`` as the message.
+
+This is very handy during development, as it catches common coding errors
+early on.  However, it has a significant impact on performance, and is
+disabled when python is run with the ``-O`` option.
+
+You can also disable this error check by setting the following option `before`
+importing ``pyglet.gl`` or ``pyglet.window``::
+
+    # Disable error checking for increased performance
+    pyglet.options['debug_gl'] = False
+
+    from pyglet.gl import *
+
+Setting the option after importing ``pyglet.gl`` will have no effect.  Once
+disabled, there is no error-checking overhead in each GL call.
+
+Using extension functions
+-------------------------
+
+Before using an extension function, you should check that the extension is
+implemented by the current driver.  Typically this is done using
+``glGetString(GL_EXTENSIONS)``, but pyglet has a convenience module,
+`pyglet.gl.gl_info` that does this for you::
+
+    if pyglet.gl.gl_info.have_extension('GL_ARB_shadow'):
+        # ... do shadow-related code.
+    else:
+        # ... raise an exception, or use a fallback method
+
+You can also easily check the version of OpenGL::
+
+    if pyglet.gl.gl_info.have_version(1,5):
+        # We can assume all OpenGL 1.5 functions are implemented.
+
+Remember to only call the ``gl_info`` functions after creating a window.
+
+There is a corresponding ``glu_info`` module for checking the version and
+extensions of GLU.
+
+nVidia often release hardware with extensions before having them registered
+officially.  When you ``import * from pyglet.gl`` you import only the
+registered extensions.  You can import the latest nVidia extensions
+with::
+
+    from pyglet.gl.glext_nv import *
+
+Using multiple windows
+----------------------
+
+pyglet allows you to create and display any number of windows simultaneously.
+Each will be created with its own OpenGL context, however all contexts will
+share the same texture objects, display lists, shader programs, and so on,
+by default [#objects]_.  Each context has its own state and framebuffers.
+
+There is always an active context (unless there are no windows).  When using
+:py:func:`pyglet.app.run` for the application event loop, pyglet ensures that
+the correct window is the active context before dispatching the
+:py:meth:`~pyglet.window.Window.on_draw` or
+:py:meth:`~pyglet.window.Window.on_resize` events.
+
+In other cases, you can explicitly set the active context with
+:py:class:`pyglet.window.Window.switch_to`.
+
+.. [#objects] Sometimes objects and lists cannot be shared between contexts; for
+              example, when the contexts are provided by different video
+              devices.  This will usually only occur if you explicitly select
+              different screens driven by different devices.
+
+AGL, GLX and WGL
+----------------
+
+The OpenGL context itself is managed by an operating-system specific library:
+AGL on OS X, GLX under X11 and WGL on Windows.  pyglet handles these details
+when a window is created, but you may need to use the functions directly (for
+example, to use pbuffers) or an extension function.
+
+The modules are named ``pyglet.gl.agl``, ``pyglet.gl.glx`` and
+``pyglet.gl.wgl``.  You must only import the correct module for the running
+operating system::
+
+    if sys.platform.startswith('linux'):
+        from pyglet.gl.glx import *
+        glxCreatePbuffer(...)
+    elif sys.platform == 'darwin':
+        from pyglet.gl.agl import *
+        aglCreatePbuffer(...)
+
+Alternativally you can use :py:attr:`pyglet.compat_platform` to support
+platforms that are compatible with platforms not officially supported
+by pyglet. For example FreeBSD systems will appear as ``linux-compat``
+in ``pyglet.compat_platform``.
+
+There are convenience modules for querying the version and extensions of WGL
+and GLX named ``pyglet.gl.wgl_info`` and ``pyglet.gl.glx_info``, respectively.
+AGL does not have such a module, just query the version of OS X instead.
+
+If using GLX extensions, you can import ``pyglet.gl.glxext_arb`` for the
+registered extensions or ``pyglet.gl.glxext_nv`` for the latest nVidia
+extensions.
+
+Similarly, if using WGL extensions, import ``pyglet.gl.wglext_arb`` or
+``pyglet.gl.wglext_nv``.
diff --git a/doc/programming_guide/graphics.rst b/doc/programming_guide/graphics.rst
new file mode 100644
index 0000000..9c5211b
--- /dev/null
+++ b/doc/programming_guide/graphics.rst
@@ -0,0 +1,571 @@
+.. _guide_graphics:
+
+Graphics
+========
+
+At the lowest level, pyglet uses OpenGL to draw graphics in program windows.
+The OpenGL interface is exposed via the :py:mod:`pyglet.gl` module
+(see :ref:`guide_gl`).
+
+Using the OpenGL interface directly, however, can be difficult to do
+efficiently. The :py:mod:`pyglet.graphics` module provides a simpler means
+for drawing graphics that uses vertex arrays and vertex buffer objects
+internally to deliver better performance.
+
+Drawing primitives
+------------------
+
+The :py:mod:`pyglet.graphics` module draws the OpenGL primitive objects by
+a mode denoted by the constants
+
+* ``pyglet.gl.GL_POINTS``
+* ``pyglet.gl.GL_LINES``
+* ``pyglet.gl.GL_LINE_LOOP``
+* ``pyglet.gl.GL_LINE_STRIP``
+* ``pyglet.gl.GL_TRIANGLES``
+* ``pyglet.gl.GL_TRIANGLE_STRIP``
+* ``pyglet.gl.GL_TRIANGLE_FAN``
+* ``pyglet.gl.GL_QUADS``
+* ``pyglet.gl.GL_QUAD_STRIP``
+* ``pyglet.gl.GL_POLYGON``
+
+See the `OpenGL Programming Guide <http://www.glprogramming.com/red/>`_ for a
+description of each of mode.
+
+Each primitive is made up of one or more vertices.  Each vertex is specified
+with either 2, 3 or 4 components (for 2D, 3D, or non-homogeneous coordinates).
+The data type of each component can be either int or float.
+
+Use :py:func:`pyglet.graphics.draw` to directly draw a primitive.
+The following example draws two points at coordinates (10, 15) and (30, 35)::
+
+    pyglet.graphics.draw(2, pyglet.gl.GL_POINTS,
+        ('v2i', (10, 15, 30, 35))
+    )
+
+The first and second arguments to the function give the number of vertices to
+draw and the primitive mode, respectively.  The third argument is a "data
+item", and gives the actual vertex data.
+
+However, because of the way the graphics API renders multiple primitives with
+shared state, ``GL_POLYGON``, ``GL_LINE_LOOP`` and ``GL_TRIANGLE_FAN`` cannot
+be used --- the results are undefined.
+
+Alternatively, the ``NV_primitive_restart`` extension can be used if it is
+present.  This also permits use of ``GL_POLYGON``, ``GL_LINE_LOOP`` and
+``GL_TRIANGLE_FAN``.   Unfortunately the extension is not provided by older
+video drivers, and requires indexed vertex lists.
+
+Because vertex data can be supplied in several forms, a "format string" is
+required.  In this case, the format string is ``"v2i"``, meaning the vertex
+position data has two components (2D) and int type.
+
+The following example has the same effect as the previous one, but uses
+floating point data and 3 components per vertex::
+
+    pyglet.graphics.draw(2, pyglet.gl.GL_POINTS,
+        ('v3f', (10.0, 15.0, 0.0, 30.0, 35.0, 0.0))
+    )
+
+Vertices can also be drawn out of order and more than once by using the
+:py:func:`pyglet.graphics.draw_indexed` function.  This requires a list of
+integers giving the indices into the vertex data.  The following example
+draws the same two points as above, but indexes the vertices (sequentially)::
+
+    pyglet.graphics.draw_indexed(2, pyglet.gl.GL_POINTS,
+        [0, 1],
+        ('v2i', (10, 15, 30, 35))
+    )
+
+This second example is more typical; two adjacent triangles are drawn, and the
+shared vertices are reused with indexing::
+
+    pyglet.graphics.draw_indexed(4, pyglet.gl.GL_TRIANGLES,
+        [0, 1, 2, 0, 2, 3],
+        ('v2i', (100, 100,
+                 150, 100,
+                 150, 150,
+                 100, 150))
+    )
+
+Note that the first argument gives the number of vertices in the data, not the
+number of indices (which is implicit on the length of the index list given in
+the third argument).
+
+When using ``GL_LINE_STRIP``, ``GL_TRIANGLE_STRIP`` or ``GL_QUAD_STRIP`` care
+must be taken to insert degenerate vertices at the beginning and end of each
+vertex list.  For example, given the vertex list::
+
+    A, B, C, D
+
+the correct vertex list to provide the vertex list is::
+
+    A, A, B, C, D, D
+
+
+Vertex attributes
+-----------------
+
+Besides the required vertex position, vertices can have several other numeric
+attributes.  Each is specified in the format string with a letter, the number
+of components and the data type.
+
+Each of the attributes is described in the table below with the set of valid
+format strings written as a regular expression (for example, ``"v[234][if]"``
+means ``"v2f"``, ``"v3i"``, ``"v4f"``, etc. are all valid formats).
+
+Some attributes have a "recommended" format string, which is the most efficient
+form for the video driver as it requires less conversion.
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Attribute
+          - Formats
+          - Recommended
+        * - Vertex position
+          - ``"v[234][sifd]"``
+          - ``"v[234]f"``
+        * - Color
+          - ``"c[34][bBsSiIfd]"``
+          - ``"c[34]B"``
+        * - Edge flag
+          - ``"e1[bB]"``
+          -
+        * - Fog coordinate
+          - ``"f[1234][bBsSiIfd]"``
+          -
+        * - Normal
+          - ``"n3[bsifd]"``
+          - ``"n3f"``
+        * - Secondary color
+          - ``"s[34][bBsSiIfd]"``
+          - ``"s[34]B"``
+        * - Texture coordinate
+          - ``"[0-31]?t[234][sifd]"``
+          - ``"[0-31]?t[234]f"``
+        * - Generic attribute
+          - ``"[0-15]g(n)?[1234][bBsSiIfd]"``
+          -
+
+The possible data types that can be specified in the format string are
+described below.
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Format
+          - Type
+          - Python type
+        * - ``"b"``
+          - Signed byte
+          - int
+        * - ``"B"``
+          - Unsigned byte
+          - int
+        * - ``"s"``
+          - Signed short
+          - int
+        * - ``"S"``
+          - Unsigned short
+          - int
+        * - ``"i"``
+          - Signed int
+          - int
+        * - ``"I"``
+          - Unsigned int
+          - int
+        * - ``"f"``
+          - Single precision float
+          - float
+        * - ``"d"``
+          - Double precision float
+          - float
+
+The following attributes are normalised to the range ``[0, 1]``.  The value is
+used as-is if the data type is floating-point.  If the data type is byte,
+short or int, the value is divided by the maximum value representable by that
+type.  For example, unsigned bytes are divided by 255 to get the normalised
+value.
+
+* Color
+* Secondary color
+* Generic attributes with the ``"n"`` format given.
+
+Texture coordinate attributes may optionally be preceded by a texture unit
+number.  If unspecified, texture unit 0 (``GL_TEXTURE0``) is implied.  It is
+the application's responsibility to ensure that the OpenGL version is adequate
+and that the specified texture unit is within the maximum allowed by the
+implementation.
+
+Up to 16 generic attributes can be specified per vertex, and can be used by
+shader programs for any purpose (they are ignored in the fixed-function
+pipeline).  For the other attributes, consult the OpenGL programming guide for
+details on their effects.
+
+When using the `pyglet.graphics.draw` and related functions, attribute data is
+specified alongside the vertex position data.  The following example
+reproduces the two points from the previous page, except that the first point
+is blue and the second green::
+
+    pyglet.graphics.draw(2, pyglet.gl.GL_POINTS,
+        ('v2i', (10, 15, 30, 35)),
+        ('c3B', (0, 0, 255, 0, 255, 0))
+    )
+
+It is an error to provide more than one set of data for any attribute, or to
+mismatch the size of the initial data with the number of vertices specified in
+the first argument.
+
+Vertex lists
+------------
+
+There is a significant overhead in using :py:func:`pyglet.graphics.draw` and
+:py:func:`pyglet.graphics.draw_indexed` due to pyglet interpreting and
+formatting the vertex data for the video device.  Usually the data drawn in
+each frame (of an animation) is identical or very similar to the previous
+frame, so this overhead is unnecessarily repeated.
+
+A :py:class:`~pyglet.graphics.vertexdomain.VertexList` is a list of vertices
+and their attributes, stored in an efficient manner that's suitable for
+direct upload to the video card. On newer video cards (supporting
+OpenGL 1.5 or later) the data is actually stored in video memory.
+
+Create a :py:class:`~pyglet.graphics.vertexdomain.VertexList` for a set of
+attributes and initial data with :py:func:`pyglet.graphics.vertex_list`.
+The following example creates a vertex list with the two coloured points
+used in the previous page::
+
+    vertex_list = pyglet.graphics.vertex_list(2,
+        ('v2i', (10, 15, 30, 35)),
+        ('c3B', (0, 0, 255, 0, 255, 0))
+    )
+
+To draw the vertex list, call its :py:meth:`~pyglet.graphics.vertexdomain.VertexList.draw` method::
+
+    vertex_list.draw(pyglet.gl.GL_POINTS)
+
+Note that the primitive mode is given to the draw method, not the vertex list
+constructor.  Otherwise the :py:func:`pyglet.graphics.vertex_list` function
+takes the same arguments as :py:class:`pyglet.graphics.draw`, including
+any number of vertex attributes.
+
+Because vertex lists can reside in video memory, it is necessary to call the
+`delete` method to release video resources if the vertex list isn't going to
+be used any more (there's no need to do this if you're just exiting the
+process).
+
+Updating vertex data
+^^^^^^^^^^^^^^^^^^^^
+
+The data in a vertex list can be modified.  Each vertex attribute (including
+the vertex position) appears as an attribute on the
+:py:class:`~pyglet.graphics.vertexdomain.VertexList` object.
+The attribute names are given in the following table.
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Vertex attribute
+          - Object attribute
+        * - Vertex position
+          - ``vertices``
+        * - Color
+          - ``colors``
+        * - Edge flag
+          - ``edge_flags``
+        * - Fog coordinate
+          - ``fog_coords``
+        * - Normal
+          - ``normals``
+        * - Secondary color
+          - ``secondary_colors``
+        * - Texture coordinate
+          - ``tex_coords`` [#multitex]_
+        * - Generic attribute
+          - *Inaccessible*
+
+In the following example, the vertex positions of the vertex list are updated
+by replacing the ``vertices`` attribute::
+
+    vertex_list.vertices = [20, 25, 40, 45]
+
+The attributes can also be selectively updated in-place::
+
+    vertex_list.vertices[:2] = [30, 35]
+
+Similarly, the color attribute of the vertex can be updated::
+
+    vertex_list.colors[:3] = [255, 0, 0]
+
+For large vertex lists, updating only the modified vertices can have a
+perfomance benefit, especially on newer graphics cards.
+
+Attempting to set the attribute list to a different size will cause an error
+(not necessarily immediately, either).  To resize the vertex list, call
+`VertexList.resize` with the new vertex count.  Be sure to fill in any
+newly uninitialised data after resizing the vertex list.
+
+Since vertex lists are mutable, you may not necessarily want to initialise
+them with any particular data.  You can specify just the format string in
+place of the ``(format, data)`` tuple in the data arguments `vertex_list`
+function.  The following example creates a vertex list of 1024 vertices with
+positional, color, texture coordinate and normal attributes::
+
+    vertex_list = pyglet.graphics.vertex_list(1024, 'v3f', 'c4B', 't2f', 'n3f')
+
+Data usage
+^^^^^^^^^^
+
+By default, pyglet assumes vertex data will be updated less often than it is
+drawn, but more often than just during initialisation.  You can override
+this assumption for each attribute by affixing a usage specification
+onto the end of the format string, detailed in the following table:
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Usage
+          - Description
+        * - ``"/static"``
+          - Data is never or rarely modified after initialisation
+        * - ``"/dynamic"``
+          - Data is occasionally modified (default)
+        * - ``"/stream"``
+          - Data is updated every frame
+
+In the following example a vertex list is created in which the positional data
+is expected to change every frame, but the color data is expected to remain
+relatively constant::
+
+    vertex_list = pyglet.graphics.vertex_list(1024, 'v3f/stream', 'c4B/static')
+
+The usage specification affects how pyglet lays out vertex data in memory,
+whether or not it's stored on the video card, and is used as a hint to OpenGL.
+Specifying a usage does not affect what operations are possible with a vertex
+list (a ``static`` attribute can still be modified), and may only have
+performance benefits on some hardware.
+
+Indexed vertex lists
+^^^^^^^^^^^^^^^^^^^^
+
+:py:class:`~pyglet.graphics.vertexdomain.IndexedVertexList` performs the same
+role as :py:class:`~pyglet.graphics.vertexdomain.VertexList`, but for indexed
+vertices.  Use :py:func:`pyglet.graphics.vertex_list_indexed` to construct an
+indexed vertex list, and update the
+:py:class:`~pyglet.graphics.vertexdomain.IndexedVertexList.indices` sequence to
+change the indices.
+
+.. [#multitex] Only texture coordinates for texture unit 0 are accessible
+    through this attribute.
+
+.. _guide_batched-rendering:
+
+Batched rendering
+-----------------
+
+For optimal OpenGL performance, you should render as many vertex lists as
+possible in a single ``draw`` call.  Internally, pyglet uses
+:py:class:`~pyglet.graphics.vertexdomain.VertexDomain` and
+:py:class:`~pyglet.graphics.vertexdomain.IndexedVertexDomain` to keep vertex
+lists that share the same attribute formats in adjacent areas of memory.
+The entire domain of vertex lists can then be drawn at once, without calling
+:py:meth:`~pyglet.graphics.vertexdomain.VertexList.draw` on each individual
+list.
+
+It is quite difficult and tedious to write an application that manages vertex
+domains itself, though.  In addition to maintaining a vertex domain for each
+set of attribute formats, domains must also be separated by primitive mode and
+required OpenGL state.
+
+The :py:class:`~pyglet.graphics.Batch` class implements this functionality,
+grouping related vertex lists together and sorting by OpenGL state
+automatically. A batch is created with no arguments::
+
+    batch = pyglet.graphics.Batch()
+
+Vertex lists can now be created with the :py:meth:`~pyglet.graphics.Batch.add`
+and :py:meth:`~pyglet.graphics.Batch.add_indexed` methods instead of
+:py:func:`pyglet.graphics.vertex_list` and
+:py:func:`pyglet.graphics.vertex_list_indexed` functions.  Unlike the module
+functions, these methods accept a ``mode`` parameter (the primitive mode)
+and a ``group`` parameter (described below).
+
+The two coloured points from previous pages can be added to a batch as a
+single vertex list with::
+
+    vertex_list = batch.add(2, pyglet.gl.GL_POINTS, None,
+        ('v2i', (10, 15, 30, 35)),
+        ('c3B', (0, 0, 255, 0, 255, 0))
+    )
+
+The resulting `vertex_list` can be modified as described in the previous
+section.  However, instead of calling ``VertexList.draw`` to draw it, call
+``Batch.draw()`` to draw all vertex lists contained in the batch at once::
+
+    batch.draw()
+
+For batches containing many vertex lists this gives a significant performance
+improvement over drawing individual vertex lists.
+
+To remove a vertex list from a batch, call ``VertexList.delete()``. If you
+don't need to modify or delete vertex lists after adding them to the batch,
+you can simply ignore the return value of the
+:py:meth:`~pyglet.graphics.Batch.add` and
+:py:meth:`~pyglet.graphics.Batch.add_indexed` methods.
+
+Setting the OpenGL state
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+In order to achieve many effects in OpenGL one or more global state parameters
+must be set.  For example, to enable and bind a texture requires::
+
+    from pyglet.gl import *
+    glEnable(texture.target)
+    glBindTexture(texture.target, texture.id)
+
+before drawing vertex lists, and then::
+
+    glDisable(texture.target)
+
+afterwards to avoid interfering with later drawing commands.
+
+With a :py:class:`~pyglet.graphics.Group` these state changes can be
+encapsulated and associated with the vertex lists they affect.
+Subclass :py:class:`~pyglet.graphics.Group` and override the `Group.set_state`
+and `Group.unset_state` methods to perform the required state changes::
+
+    class CustomGroup(pyglet.graphics.Group):
+        def set_state(self):
+            glEnable(texture.target)
+            glBindTexture(texture.target, texture.id)
+
+        def unset_state(self):
+            glDisable(texture.target)
+
+An instance of this group can now be attached to vertex lists in the batch::
+
+    custom_group = CustomGroup()
+    vertex_list = batch.add(2, pyglet.gl.GL_POINTS, custom_group,
+        ('v2i', (10, 15, 30, 35)),
+        ('c3B', (0, 0, 255, 0, 255, 0))
+    )
+
+The :py:class:`~pyglet.graphics.Batch` ensures that the appropriate
+``set_state`` and ``unset_state`` methods are called before and after
+the vertex lists that use them.
+
+Hierarchical state
+^^^^^^^^^^^^^^^^^^
+
+Groups have a `parent` attribute that allows them to be implicitly organised
+in a tree structure.  If groups **B** and **C** have parent **A**, then the
+order of ``set_state`` and ``unset_state`` calls for vertex lists in a batch
+will be::
+
+    A.set_state()
+    # Draw A vertices
+    B.set_state()
+    # Draw B vertices
+    B.unset_state()
+    C.set_state()
+    # Draw C vertices
+    C.unset_state()
+    A.unset_state()
+
+This is useful to group state changes into as few calls as possible.  For
+example, if you have a number of vertex lists that all need texturing enabled,
+but have different bound textures, you could enable and disable texturing in
+the parent group and bind each texture in the child groups.  The following
+example demonstrates this::
+
+    class TextureEnableGroup(pyglet.graphics.Group):
+        def set_state(self):
+            glEnable(GL_TEXTURE_2D)
+
+        def unset_state(self):
+            glDisable(GL_TEXTURE_2D)
+
+    texture_enable_group = TextureEnableGroup()
+
+    class TextureBindGroup(pyglet.graphics.Group):
+        def __init__(self, texture):
+            super(TextureBindGroup, self).__init__(parent=texture_enable_group)
+            assert texture.target = GL_TEXTURE_2D
+            self.texture = texture
+
+        def set_state(self):
+            glBindTexture(GL_TEXTURE_2D, self.texture.id)
+
+        # No unset_state method required.
+
+        def __eq__(self, other):
+            return (self.__class__ is other.__class__ and
+                    self.texture.id == other.texture.id and
+                    self.texture.target == other.texture.target and
+                    self.parent == other.parent)
+
+        def __hash__(self):
+            return hash((self.texture.id, self.texture.target))
+
+    batch.add(4, GL_QUADS, TextureBindGroup(texture1), 'v2f', 't2f')
+    batch.add(4, GL_QUADS, TextureBindGroup(texture2), 'v2f', 't2f')
+    batch.add(4, GL_QUADS, TextureBindGroup(texture1), 'v2f', 't2f')
+
+Note the use of an ``__eq__`` method on the group to allow
+:py:class:`~pyglet.graphics.Batch` to merge the two ``TextureBindGroup``
+identical instances.
+
+Sorting vertex lists
+^^^^^^^^^^^^^^^^^^^^
+
+:py:class:`~pyglet.graphics.vertexdomain.VertexDomain` does not attempt
+to keep vertex lists in any particular order. So, any vertex lists sharing
+the same primitive mode, attribute formats and group will be drawn in an
+arbitrary order.  However, :py:class:`~pyglet.graphics.Batch` will sort
+:py:class:`~pyglet.graphics.Group` objects sharing the same parent by
+their ``__cmp__`` method.  This allows groups to be ordered.
+
+The :py:class:`~pyglet.graphics.OrderedGroup` class is a convenience
+group that does not set any OpenGL state, but is parameterised by an
+integer giving its draw order.  In the following example a number of
+vertex lists are grouped into a "background" group that is drawn before
+the vertex lists in the "foreground" group::
+
+    background = pyglet.graphics.OrderedGroup(0)
+    foreground = pyglet.graphics.OrderedGroup(1)
+
+    batch.add(4, GL_QUADS, foreground, 'v2f')
+    batch.add(4, GL_QUADS, background, 'v2f')
+    batch.add(4, GL_QUADS, foreground, 'v2f')
+    batch.add(4, GL_QUADS, background, 'v2f', 'c4B')
+
+By combining hierarchical groups with ordered groups it is possible to
+describe an entire scene within a single :py:class:`~pyglet.graphics.Batch`,
+which then renders it as efficiently as possible.
+
+Batches and groups in other modules
+-----------------------------------
+
+The :py:class:`~pyglet.sprite.Sprite`, :py:class:`~pyglet.text.Label` and
+:py:class:`~pyglet.text.layout.TextLayout` classes all accept ``batch`` and
+``group`` parameters in their constructors.  This allows you to add any of
+these higher-level pyglet drawables into arbitrary places in your rendering
+code.
+
+For example, multiple sprites can be grouped into a single batch and then
+drawn at once, instead of calling ``Sprite.draw()`` on each one individually::
+
+    batch = pyglet.graphics.Batch()
+    sprites = [pyglet.sprite.Sprite(image, batch=batch) for i in range(100)]
+
+    batch.draw()
+
+The ``group`` parameter can be used to set the drawing order (and hence which
+objects overlap others) within a single batch, as described  on the previous
+page.
+
+In general you should batch all drawing objects into as few batches as
+possible, and use groups to manage the draw order and other OpenGL state
+changes for optimal performance.   If you are creating your own drawable
+classes, consider adding ``batch`` and ``group`` parameters in a similar way.
diff --git a/doc/programming_guide/image.rst b/doc/programming_guide/image.rst
new file mode 100644
index 0000000..00fb3af
--- /dev/null
+++ b/doc/programming_guide/image.rst
@@ -0,0 +1,968 @@
+Images
+======
+
+pyglet provides functions for loading and saving images in various formats
+using native operating system services.  If the `Pillow`_ library is installed,
+many additional formats can be supported.   pyglet also includes built-in
+codecs for loading PNG and BMP without external dependencies.
+
+Loaded images can be efficiently provided to OpenGL as a texture, and OpenGL
+textures and framebuffers can be retrieved as pyglet images to be saved or
+otherwise manipulated.
+
+If you've done any game or graphics programming, you're probably familiar with
+the concept of "sprites".  pyglet also provides an efficient and comprehensive
+:py:class:`~pyglet.sprite.Sprite` class, for displaying images on the screen
+with an optional transform (such as scaling and rotation). If you're planning
+to do anything with images that involves movement and placement on screen,
+you'll likely want to use sprites.
+
+.. _Pillow: https://pillow.readthedocs.io
+
+Loading an image
+----------------
+
+Images can be loaded using the :py:func:`pyglet.image.load` function::
+
+    kitten = pyglet.image.load('kitten.png')
+
+If you are distributing your application with included images, consider
+using the :py:mod:`pyglet.resource` module (see  :ref:`guide_resources`).
+
+Without any additional arguments, :py:func:`pyglet.image.load` will
+attempt to load the filename specified using any available image decoder.
+This will allow you to load PNG, GIF, JPEG, BMP and DDS files,
+and possibly other files as well, depending on your operating system
+and additional installed modules (see the next section for details).
+If the image cannot be loaded, an
+:py:class:`~pyglet.image.codecs.ImageDecodeException` will be raised.
+
+You can load an image from any file-like object providing a `read` method by
+specifying the `file` keyword parameter::
+
+    kitten_stream = open('kitten.png', 'rb')
+    kitten = pyglet.image.load('kitten.png', file=kitten_stream)
+
+In this case the filename ``kitten.png`` is optional, but gives a hint to
+the decoder as to the file type (it is otherwise unused when a file object
+is provided).
+
+pyglet provides the following image decoders:
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Module
+          - Class
+          - Description
+        * - ``pyglet.image.codecs.dds``
+          - ``DDSImageDecoder``
+          - Reads Microsoft DirectDraw Surface files containing compressed
+            textures
+        * - ``pyglet.image.codecs.gdiplus``
+          - ``GDIPlusDecoder``
+          - Uses Windows GDI+ services to decode images.
+        * - ``pyglet.image.codecs.gdkpixbuf2``
+          - ``GdkPixbuf2ImageDecoder``
+          - Uses the GTK-2.0 GDK functions to decode images.
+        * - ``pyglet.image.codecs.pil``
+          - ``PILImageDecoder``
+          - Wrapper interface around PIL Image class.
+        * - ``pyglet.image.codecs.quicktime``
+          - ``QuickTimeImageDecoder``
+          - Uses Mac OS X QuickTime to decode images.
+        * - ``pyglet.image.codecs.png``
+          - ``PNGImageDecoder``
+          - BMP decoder written in pure Python.
+        * - ``pyglet.image.codecs.bmp``
+          - ``BMPImageDecoder``
+          - BMP decoder written in pure Python.
+
+Each of these classes registers itself with :py:mod:`pyglet.image` with
+the filename extensions it supports.  The :py:func:`~pyglet.image.load`
+function will try each image decoder with a matching file extension first,
+before attempting the other decoders.  Only if every image decoder fails
+to load an image will :py:class:`~pyglet.image.codecs.ImageDecodeException`
+be raised (the origin of the exception will be the first decoder that
+was attempted).
+
+You can override this behaviour and specify a particular decoding instance to
+use.  For example, in the following example the pure Python PNG decoder is
+always used rather than the operating system's decoder::
+
+    from pyglet.image.codecs.png import PNGImageDecoder
+    kitten = pyglet.image.load('kitten.png', decoder=PNGImageDecoder())
+
+This use is not recommended unless your application has to work around
+specific deficiences in an operating system decoder.
+
+Supported image formats
+-----------------------
+
+The following table lists the image formats that can be loaded on each
+operating system.  If Pillow is installed, any additional formats it
+supports can also be read.  See the `Pillow docs`_ for a list of such
+formats.
+
+.. _Pillow docs: http://pillow.readthedocs.io/
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Extension
+          - Description
+          - Windows
+          - Mac OS X
+          - Linux [#linux]_
+        * - ``.bmp``
+          - Windows Bitmap
+          - X
+          - X
+          - X
+        * - ``.dds``
+          - Microsoft DirectDraw Surface [#dds]_
+          - X
+          - X
+          - X
+        * - ``.exif``
+          - Exif
+          - X
+          -
+          -
+        * - ``.gif``
+          - Graphics Interchange Format
+          - X
+          - X
+          - X
+        * - ``.jpg .jpeg``
+          - JPEG/JIFF Image
+          - X
+          - X
+          - X
+        * - ``.jp2 .jpx``
+          - JPEG 2000
+          -
+          - X
+          -
+        * - ``.pcx``
+          - PC Paintbrush Bitmap Graphic
+          -
+          - X
+          -
+        * - ``.png``
+          - Portable Network Graphic
+          - X
+          - X
+          - X
+        * - ``.pnm``
+          - PBM Portable Any Map Graphic Bitmap
+          -
+          -
+          - X
+        * - ``.ras``
+          - Sun raster graphic
+          -
+          -
+          - X
+        * - ``.tga``
+          - Truevision Targa Graphic
+          -
+          - X
+          -
+        * - ``.tif .tiff``
+          - Tagged Image File Format
+          - X
+          - X
+          - X
+        * - ``.xbm``
+          - X11 bitmap
+          -
+          - X
+          - X
+        * - ``.xpm``
+          - X11 icon
+          -
+          - X
+          - X
+
+The only supported save format is PNG, unless PIL is installed, in which case
+any format it supports can be written.
+
+.. [#linux] Requires GTK 2.0 or later.
+
+.. [#dds] Only S3TC compressed surfaces are supported.  Depth, volume and cube
+          textures are not supported.
+
+Working with images
+-------------------
+
+The :py:func:`pyglet.image.load` function returns an
+:py:class:`~pyglet.image.AbstractImage`. The actual class of the object depends
+on the decoder that was used, but all loaded imageswill have the following
+attributes:
+
+`width`
+    The width of the image, in pixels.
+`height`
+    The height of the image, in pixels.
+`anchor_x`
+    Distance of the anchor point from the left edge of the image, in pixels
+`anchor_y`
+    Distance of the anchor point from the bottom edge of the image, in pixels
+
+The anchor point defaults to (0, 0), though some image formats may contain an
+intrinsic anchor point.  The anchor point is used to align the image to a
+point in space when drawing it.
+
+You may only want to use a portion of the complete image.  You can use the
+:py:meth:`~pyglet.image.AbstractImage.get_region` method to return an image
+of a rectangular region of a source image::
+
+    image_part = kitten.get_region(x=10, y=10, width=100, height=100)
+
+This returns an image with dimensions 100x100.  The region extracted from
+`kitten` is aligned such that the bottom-left corner of the rectangle is 10
+pixels from the left and 10 pixels from the bottom of the image.
+
+Image regions can be used as if they were complete images.  Note that changes
+to an image region may or may not be reflected on the source image, and
+changes to the source image may or may not be reflected on any region images.
+You should not assume either behaviour.
+
+The AbstractImage hierarchy
+---------------------------
+
+The following sections deal with the various concrete image classes.  All
+images subclass :py:class:`~pyglet.image.AbstractImage`, which provides
+the basic interface described in previous sections.
+
+.. figure:: img/abstract_image.png
+
+    The :py:class:`~pyglet.image.AbstractImage` class hierarchy.
+
+An image of any class can be converted into a :py:class:`~pyglet.image.Texture`
+or :py:class:`~pyglet.image.ImageData` using the
+:py:meth:`~pyglet.image.AbstractImage.get_texture` and
+:py:meth:`~pyglet.image.ImageData.get_image_data` methods defined on
+:py:class:`~pyglet.image.AbstractImage`.  For example, to load an image
+and work with it as an OpenGL texture::
+
+    kitten = pyglet.image.load('kitten.png').get_texture()
+
+There is no penalty for accessing one of these methods if object is already
+of the requested class.  The following table shows how concrete classes are
+converted into other classes:
+
+    .. list-table::
+        :header-rows: 1
+        :stub-columns: 1
+
+        * - Original class
+          - ``.get_texture()``
+          - ``.get_image_data()``
+        * - :py:class:`~pyglet.image.Texture`
+          - No change
+          - ``glGetTexImage2D``
+        * - :py:class:`~pyglet.image.TextureRegion`
+          - No change
+          - ``glGetTexImage2D``, crop resulting image.
+        * - :py:class:`~pyglet.image.ImageData`
+          - ``glTexImage2D`` [1]_
+          - No change
+        * - :py:class:`~pyglet.image.ImageDataRegion`
+          - ``glTexImage2D`` [1]_
+          - No change
+        * - :py:class:`~pyglet.image.CompressedImageData`
+          - ``glCompressedTexImage2D`` [2]_
+          - N/A [3]_
+        * - :py:class:`~pyglet.image.BufferImage`
+          - ``glCopyTexSubImage2D`` [4]_
+          - ``glReadPixels``
+
+You should try to avoid conversions which use ``glGetTexImage2D`` or
+``glReadPixels``, as these can impose a substantial performance penalty by
+transferring data in the "wrong" direction of the video bus, especially on
+older hardware.
+
+.. [1]  :py:class:`~pyglet.image.ImageData` caches the texture for future use, so there is no
+        performance penalty for repeatedly blitting an
+        :py:class:`~pyglet.image.ImageData`.
+
+.. [2]  If the required texture compression extension is not present, the
+        image is decompressed in memory and then supplied to OpenGL via
+        ``glTexImage2D``.
+
+.. [3]  It is not currently possible to retrieve :py:class:`~pyglet.image.ImageData` for compressed
+        texture images.  This feature may be implemented in a future release
+        of pyglet.  One workaround is to create a texture from the compressed
+        image, then read the image data from the texture; i.e.,
+        ``compressed_image.get_texture().get_image_data()``.
+
+.. [4]  :py:class:`~pyglet.image.BufferImageMask` cannot be converted to
+        :py:class:`~pyglet.image.Texture`.
+
+Accessing or providing pixel data
+---------------------------------
+
+The :py:class:`~pyglet.image.ImageData` class represents an image as a string
+or sequence of pixel data, or as a ctypes pointer.  Details such as the pitch
+and component layout are also stored in the class.  You can access an
+:py:class:`~pyglet.image.ImageData` object for any image with
+:py:meth:`~pyglet.image.ImageData.get_image_data`::
+
+    kitten = pyglet.image.load('kitten.png').get_image_data()
+
+The design of :py:class:`~pyglet.image.ImageData` is to allow applications
+to access the detail in the format they prefer, rather than having to
+understand the many formats that each operating system and OpenGL make use of.
+
+The `pitch` and `format` properties determine how the bytes are arranged.
+`pitch` gives the number of bytes between each consecutive row.  The data is
+assumed to run from left-to-right, bottom-to-top, unless `pitch` is negative,
+in which case it runs from left-to-right, top-to-bottom.  There is no need for
+rows to be tightly packed; larger `pitch` values are often used to align each
+row to machine word boundaries.
+
+The `format` property gives the number and order of color components.  It is a
+string of one or more of the letters corresponding to the components in the
+following table:
+
+    = ============
+    R Red
+    G Green
+    B Blue
+    A Alpha
+    L Luminance
+    I Intensity
+    = ============
+
+For example, a format string of ``"RGBA"`` corresponds to four bytes of
+colour data, in the order red, green, blue, alpha.  Note that machine
+endianness has no impact on the interpretation of a format string.
+
+The length of a format string always gives the number of bytes per pixel.  So,
+the minimum absolute pitch for a given image is ``len(kitten.format) *
+kitten.width``.
+
+To retrieve pixel data in a particular format, use the `get_data` method,
+specifying the desired format and pitch. The following example reads tightly
+packed rows in ``RGB`` format (the alpha component, if any, will be
+discarded)::
+
+    kitten = kitten.get_image_data()
+    data = kitten.get_data('RGB', kitten.width * 3)
+
+`data` always returns a string, however pixel data can be set from a
+ctypes array, stdlib array, list of byte data, string, or ctypes pointer.
+To set the image data use `set_data`, again specifying the format and pitch::
+
+    kitten.set_data('RGB', kitten.width * 3, data)
+
+You can also create :py:class:`~pyglet.image.ImageData` directly, by providing
+each of these attributes to the constructor. This is any easy way to load
+textures into OpenGL from other programs or libraries.
+
+Performance concerns
+^^^^^^^^^^^^^^^^^^^^
+
+pyglet can use several methods to transform pixel data from one format to
+another.  It will always try to select the most efficient means.  For example,
+when providing texture data to OpenGL, the following possibilities are
+examined in order:
+
+1. Can the data be provided directly using a built-in OpenGL pixel format such
+   as ``GL_RGB`` or ``GL_RGBA``?
+2. Is there an extension present that handles this pixel format?
+3. Can the data be transformed with a single regular expression?
+4. If none of the above are possible, the image will be split into separate
+   scanlines and a regular expression replacement done on each; then the lines
+   will be joined together again.
+
+The following table shows which image formats can be used directly with steps
+1 and 2 above, as long as the image rows are tightly packed (that is, the
+pitch is equal to the width times the number of components).
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Format
+          - Required extensions
+        * - ``"I"``
+          -
+        * - ``"L"``
+          -
+        * - ``"LA"``
+          -
+        * - ``"R"``
+          -
+        * - ``"G"``
+          -
+        * - ``"B"``
+          -
+        * - ``"A"``
+          -
+        * - ``"RGB"``
+          -
+        * - ``"RGBA"``
+          -
+        * - ``"ARGB"``
+          - ``GL_EXT_bgra`` and ``GL_APPLE_packed_pixels``
+        * - ``"ABGR"``
+          - ``GL_EXT_abgr``
+        * - ``"BGR"``
+          - ``GL_EXT_bgra``
+        * - ``"BGRA"``
+          - ``GL_EXT_bgra``
+
+If the image data is not in one of these formats, a regular expression will be
+constructed to pull it into one.  If the rows are not tightly packed, or if
+the image is ordered from top-to-bottom, the rows will be split before the
+regular expression is applied.  Each of these may incur a performance penalty
+-- you should avoid such formats for real-time texture updates if possible.
+
+Image sequences and atlases
+---------------------------
+
+Sometimes a single image is used to hold several images.  For example, a
+"sprite sheet" is an image that contains each animation frame required for a
+character sprite animation.
+
+pyglet provides convenience classes for extracting the individual images from
+such a composite image as if it were a simple Python sequence.  Discrete
+images can also be packed into one or more larger textures with texture bins
+and atlases.
+
+.. figure:: img/image_sequence.png
+
+    The AbstractImageSequence class hierarchy.
+
+Image grids
+^^^^^^^^^^^
+
+An "image grid" is a single image which is divided into several smaller images
+by drawing an imaginary grid over it.  The following image shows an image used
+for the explosion animation in the *Astraea* example.
+
+.. figure:: img/explosion.png
+
+    An image consisting of eight animation frames arranged in a grid.
+
+This image has one row and eight columns.  This is all the information you
+need to create an :py:class:`~pyglet.image.ImageGrid` with::
+
+    explosion = pyglet.image.load('explosion.png')
+    explosion_seq = pyglet.image.ImageGrid(explosion, 1, 8)
+
+The images within the grid can now be accessed as if they were their own
+images::
+
+    frame_1 = explosion_seq[0]
+    frame_2 = explosion_seq[1]
+
+Images with more than one row can be accessed either as a single-dimensional
+sequence, or as a (row, column) tuple; as shown in the following diagram.
+
+.. figure:: img/image_grid.png
+
+    An image grid with several rows and columns, and the slices that can be
+    used to access it.
+
+Image sequences can be sliced like any other sequence in Python.  For example,
+the following obtains the first four frames in the animation::
+
+    start_frames = explosion_seq[:4]
+
+For efficient rendering, you should use a
+:py:class:`~pyglet.image.TextureGrid`.
+This uses a single texture for the grid, and each individual image returned
+from a slice will be a :py:class:`~pyglet.image.TextureRegion`::
+
+    explosion_tex_seq = image.TextureGrid(explosion_seq)
+
+Because :py:class:`~pyglet.image.TextureGrid` is also a
+:py:class:`~pyglet.image.Texture`, you can use it either as individual images
+or as the whole grid at once.
+
+3D textures
+^^^^^^^^^^^
+
+:py:class:`~pyglet.image.TextureGrid` is extremely efficient for drawing many
+sprites from a single texture.  One problem you may encounter, however,
+is bleeding between adjacent images.
+
+When OpenGL renders a texture to the screen, by default it obtains each pixel
+colour by interpolating nearby texels.  You can disable this behaviour by
+switching to the ``GL_NEAREST`` interpolation mode, however you then lose the
+benefits of smooth scaling, distortion, rotation and sub-pixel positioning.
+
+You can alleviate the problem by always leaving a 1-pixel clear border around
+each image frame.  This will not solve the problem if you are using
+mipmapping, however.  At this stage you will need a 3D texture.
+
+You can create a 3D texture from any sequence of images, or from an
+:py:class:`~pyglet.image.ImageGrid`.  The images must all be of the same
+dimension, however they need not be powers of two (pyglet takes care of
+this by returning :py:class:`~pyglet.image.TextureRegion`
+as with a regular :py:class:`~pyglet.image.Texture`).
+
+In the following example, the explosion texture from above is uploaded into a
+3D texture::
+
+    explosion_3d = pyglet.image.Texture3D.create_for_image_grid(explosion_seq)
+
+You could also have stored each image as a separate file and used
+:py:meth:`pyglet.image.Texture3D.create_for_images` to create the 3D texture.
+
+Once created, a 3D texture behaves like any other
+:py:class:`~pyglet.image.AbstractImageSequence`; slices return
+:py:class:`~pyglet.image.TextureRegion` for an image plane within the texture.
+Unlike a :py:class:`~pyglet.image.TextureGrid`, though, you cannot blit a
+:py:class:`~pyglet.image.Texture3D` in its entirety.
+
+.. _guide_texture-bins-and-atlases:
+
+Texture bins and atlases
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+Image grids are useful when the artist has good tools to construct the larger
+images of the appropriate format, and the contained images all have the same
+size.  However it is often simpler to keep individual images as separate files
+on disk, and only combine them into larger textures at runtime for efficiency.
+
+A :py:class:`~pyglet.image.atlas.TextureAtlas` is initially an empty texture,
+but images of any size can be added to it at any time.  The atlas takes care
+of tracking the "free" areas within the texture, and of placing images at
+appropriate locations within the texture to avoid overlap.
+
+It's possible for a :py:class:`~pyglet.image.atlas.TextureAtlas` to run out
+of space for new images, so applications will need to either know the correct
+size of the texture to allocate initally, or maintain multiple atlases as
+each one fills up.
+
+The :py:class:`~pyglet.image.atlas.TextureBin` class provides a simple means
+to manage multiple atlases. The following example loads a list of images,
+then inserts those images into a texture bin.  The resulting list is a list of
+:py:class:`~pyglet.image.TextureRegion` images that map
+into the larger shared texture atlases::
+
+    images = [
+        pyglet.image.load('img1.png'),
+        pyglet.image.load('img2.png'),
+        # ...
+    ]
+
+    bin = pyglet.image.atlas.TextureBin()
+    images = [bin.add(image) for image in images]
+
+The :py:mod:`pyglet.resource` module (see :ref:`guide_resources`) uses
+texture bins internally to efficiently pack images automatically.
+
+Animations
+----------
+
+While image sequences and atlases provide storage for related images,
+they alone are not enough to describe a complete animation.
+
+The :py:class:`~pyglet.image.Animation` class manages a list of
+:py:class:`~pyglet.image.AnimationFrame` objects, each of
+which references an image and a duration (in seconds).  The storage of
+the images is up to the application developer: they can each be discrete, or
+packed into a texture atlas, or any other technique.
+
+An animation can be loaded directly from a GIF 89a image file with
+:py:func:`~pyglet.image.load_animation` (supported on Linux, Mac OS X
+and Windows) or constructed manually from a list of images or an image
+sequence using the class methods (in which case the timing information
+will also need to be provided).
+The :py:func:`~pyglet.image.Animation.add_to_texture_bin` method provides
+a convenient way to pack the image frames into a texture bin for efficient
+access.
+
+Individual frames can be accessed by the application for use with any kind of
+rendering, or the entire animation can be used directly with a
+:py:class:`~pyglet.sprite.Sprite` (see next section).
+
+The following example loads a GIF animation and packs the images in that
+animation into a texture bin.  A sprite is used to display the animation in
+the window::
+
+    animation = pyglet.image.load_animation('animation.gif')
+    bin = pyglet.image.atlas.TextureBin()
+    animation.add_to_texture_bin(bin)
+    sprite = pyglet.sprite.Sprite(img=animation)
+
+    window = pyglet.window.Window()
+
+    @window.event
+    def on_draw():
+        window.clear()
+        sprite.draw()
+
+    pyglet.app.run()
+
+When animations are loaded with :py:mod:`pyglet.resource` (see
+:ref:`guide_resources`) the frames are automatically packed into a texture bin.
+
+This example program is located in
+`examples/programming_guide/animation.py`, along with a sample GIF animation
+file.
+
+Buffer images
+-------------
+
+pyglet provides a basic representation of the framebuffer as components of the
+:py:class:`~pyglet.image.AbstractImage` hierarchy.  At this stage this
+representation is based off OpenGL 1.1, and there is no support for newer
+features such as framebuffer objects.  Of course, this doesn't prevent you
+using framebuffer objects in your programs -- :py:mod:`pyglet.gl` provides
+this functionality -- just that they are not represented as
+:py:class:`~pyglet.image.AbstractImage` types.
+
+.. figure:: img/buffer_image.png
+
+    The :py:class:`~pyglet.image.BufferImage` hierarchy.
+
+A framebuffer consists of
+
+* One or more colour buffers, represented by
+  :py:class:`~pyglet.image.ColorBufferImage`
+* An optional depth buffer, represented by
+  :py:class:`~pyglet.image.DepthBufferImage`
+* An optional stencil buffer, with each bit represented by
+  :py:class:`~pyglet.image.BufferImageMask`
+* Any number of auxiliary buffers, also represented by
+  :py:class:`~pyglet.image.ColorBufferImage`
+
+You cannot create the buffer images directly; instead you must obtain
+instances via the :py:class:`~pyglet.image.BufferManager`.
+Use :py:func:`~pyglet.image.get_buffer_manager` to get this singleton::
+
+    buffers = image.get_buffer_manager()
+
+Only the back-left color buffer can be obtained (i.e., the front buffer is
+inaccessible, and stereo contexts are not supported by the buffer manager)::
+
+    color_buffer = buffers.get_color_buffer()
+
+This buffer can be treated like any other image.  For example, you could copy
+it to a texture, obtain its pixel data, save it to a file, and so on.  Using
+the :py:attr:`~pyglet.image.AbstractImage.texture` attribute is particularly
+useful, as it allows you to perform multipass rendering effects without
+needing a render-to-texture extension.
+
+The depth buffer can be obtained similarly::
+
+    depth_buffer = buffers.get_depth_buffer()
+
+When a depth buffer is converted to a texture, the class used will be a
+:py:class:`~pyglet.image.DepthTexture`, suitable for use with shadow map
+techniques.
+
+The auxiliary buffers and stencil bits are obtained by requesting one, which
+will then be marked as "in-use".  This permits multiple libraries and your
+application to work together without clashes in stencil bits or auxiliary
+buffer names.  For example, to obtain a free stencil bit::
+
+    mask = buffers.get_buffer_mask()
+
+The buffer manager maintains a weak reference to the buffer mask, so that when
+you release all references to it, it will be returned to the pool of available
+masks.
+
+Similarly, a free auxiliary buffer is obtained::
+
+    aux_buffer = buffers.get_aux_buffer()
+
+When using the stencil or auxiliary buffers, make sure you explicitly request
+these when creating the window.  See `OpenGL configuration options` for
+details.
+
+Displaying images
+-----------------
+
+Image drawing is usually done in the window's
+:py:meth:`~pyglet.window.Window.on_draw` event handler.
+It is possible to draw individual images directly, but usually you will
+want to create a "sprite" for each appearance of the image on-screen.
+
+Sprites
+^^^^^^^
+
+A Sprite is a full featured class for displaying instances of Images or
+Animations in the window. Image and Animation instances are mainly concerned
+with the image data (size, pixels, etc.), wheras Sprites also include
+additional properties. These include x/y location, scale, rotation, opacity,
+color tint, visibility, and both horizontal and vertical scaling.
+Multiple sprites can share the same image; for example, hundreds of bullet
+sprites might share the same bullet image.
+
+A Sprite is constructed given an image or animation, and can be directly
+drawn with the :py:meth:`~pyglet.sprite.Sprite.draw` method::
+
+    sprite = pyglet.sprite.Sprite(img=image)
+
+    @window.event
+    def on_draw():
+        window.clear()
+        sprite.draw()
+
+If created with an animation, sprites automatically handle displaying
+the most up-to-date frame of the animation.  The following example uses a
+scheduled function to gradually move the Sprite across the screen::
+
+    def update(dt):
+        # Move 10 pixels per second
+        sprite.x += dt * 10
+
+    # Call update 60 times a second
+    pyglet.clock.schedule_interval(update, 1/60.)
+
+If you need to draw many sprites, using a :py:class:`~pyglet.graphics.Batch`
+to draw them all at once is strongly recommended.  This is far more efficient
+than calling :py:meth:`~pyglet.sprite.Sprite.draw` on each of them in a loop::
+
+    batch = pyglet.graphics.Batch()
+
+    sprites = [pyglet.sprite.Sprite(image, batch=batch),
+               pyglet.sprite.Sprite(image, batch=batch),
+               # ...  ]
+
+    @window.event
+    def on_draw():
+        window.clear()
+        batch.draw()
+
+When sprites are collected into a batch, no guarantee is made about the order
+in which they will be drawn.  If you need to ensure some sprites are drawn
+before others (for example, landscape tiles might be drawn before character
+sprites, which might be drawn before some particle effect sprites), use two
+or more :py:class:`~pyglet.graphics.OrderedGroup` objects to specify the
+draw order::
+
+    batch = pyglet.graphics.Batch()
+    background = pyglet.graphics.OrderedGroup(0)
+    foreground = pyglet.graphics.OrderedGroup(1)
+
+    sprites = [pyglet.sprite.Sprite(image, batch=batch, group=background),
+               pyglet.sprite.Sprite(image, batch=batch, group=background),
+               pyglet.sprite.Sprite(image, batch=batch, group=foreground),
+               pyglet.sprite.Sprite(image, batch=batch, group=foreground),
+               # ...]
+
+    @window.event
+    def on_draw():
+        window.clear()
+        batch.draw()
+
+For best performance, you should use as few batches and groups as required.
+(See the :ref:`guide_graphics` section for more details on batch
+and group rendering). This will reduce the number of internal and OpenGL
+operations for drawing each frame.
+
+In addition, try to combine your images into as few textures as possible;
+for example, by loading images with :py:func:`pyglet.resource.image`
+(see :ref:`guide_resources`) or with :ref:`guide_texture-bins-and-atlases`).
+A common pitfall is to use the :py:func:`pyglet.image.load` method to load
+a large number of images.  This will cause a seperate texture to be created
+for each image loaded, resulting in a lot of OpenGL texture binding overhead
+for each frame.
+
+Simple image blitting
+^^^^^^^^^^^^^^^^^^^^^
+
+Drawing images directly is less efficient, but may be adequate for
+simple cases. Images can be drawn into a window with the
+:py:meth:`~pyglet.image.AbstractImage.blit` method::
+
+    @window.event
+    def on_draw():
+        window.clear()
+        image.blit(x, y)
+
+The `x` and `y` coordinates locate where to draw the anchor point of the
+image.  For example, to center the image at ``(x, y)``::
+
+    kitten.anchor_x = kitten.width // 2
+    kitten.anchor_y = kitten.height // 2
+    kitten.blit(x, y)
+
+You can also specify an optional `z` component to the
+:py:meth:`~pyglet.image.AbstractImage.blit` method.
+This has no effect unless you have changed the default projection
+or enabled depth testing.  In the following example, the second
+image is drawn *behind* the first, even though it is drawn after it::
+
+    from pyglet.gl import *
+    glEnable(GL_DEPTH_TEST)
+
+    kitten.blit(x, y, 0)
+    kitten.blit(x, y, -0.5)
+
+The default pyglet projection has a depth range of (-1, 1) -- images drawn
+with a z value outside this range will not be visible, regardless of whether
+depth testing is enabled or not.
+
+Images with an alpha channel can be blended with the existing framebuffer.  To
+do this you need to supply OpenGL with a blend equation.  The following code
+fragment implements the most common form of alpha blending, however other
+techniques are also possible::
+
+    from pyglet.gl import *
+    glEnable(GL_BLEND)
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+
+You would only need to call the code above once during your program, before
+you draw any images (this is not necessary when using only sprites).
+
+OpenGL imaging
+--------------
+
+This section assumes you are familiar with texture mapping in OpenGL (for
+example, chapter 9 of the `OpenGL Programming Guide`_).
+
+To create a texture from any :py:class:`~pyglet.image.AbstractImage`,
+call :py:meth:`~pyglet.image.AbstractImage.get_texture`::
+
+    kitten = image.load('kitten.jpg')
+    texture = kitten.get_texture()
+
+Textures are automatically created and used by
+:py:class:`~pyglet.image.ImageData` when blitted.  Itis useful to use
+textures directly when aiming for high performance or 3D applications.
+
+The :py:class:`~pyglet.image.Texture` class represents any texture object.
+The :py:attr:`~pyglet.image.TextureRegion.target` attribute gives the
+texture target (for example, ``GL_TEXTURE_2D``) and
+:py:attr:`~pyglet.image.TextureRegion.id` the texturename.
+For example, to bind a texture::
+
+    glBindTexture(texture.target, texture.id)
+
+Texture dimensions
+^^^^^^^^^^^^^^^^^^
+
+Implementations of OpenGL prior to 2.0 require textures to have dimensions
+that are powers of two (i.e., 1, 2, 4, 8, 16, ...).  Because of this
+restriction, pyglet will always create textures of these dimensions (there are
+several non-conformant post-2.0 implementations).  This could have unexpected
+results for a user blitting a texture loaded from a file of non-standard
+dimensions.  To remedy this, pyglet returns a
+:py:class:`~pyglet.image.TextureRegion` of the larger
+texture corresponding to just the part of the texture covered by the original
+image.
+
+A :py:class:`~pyglet.image.TextureRegion` has an `owner` attribute that
+references the larger texture. The following session demonstrates this::
+
+    >>> rgba = image.load('tests/image/rgba.png')
+    >>> rgba
+    <ImageData 235x257>         # The image is 235x257
+    >>> rgba.get_texture()
+    <TextureRegion 235x257>     # The returned texture is a region
+    >>> rgba.get_texture().owner
+    <Texture 256x512>           # The owning texture has power-2 dimensions
+    >>>
+
+A :py:class:`~pyglet.image.TextureRegion` defines a
+:py:attr:`~pyglet.image.TextureRegion.tex_coords` attribute that gives
+the texture coordinates to use for a quad mapping the whole image.
+:py:attr:`~pyglet.image.TextureRegion.tex_coords` is a 4-tuple of 3-tuple
+of floats; i.e., each texture coordinate is given in 3 dimensions.
+The following code can be used to render a quad for a texture region::
+
+    texture = kitten.get_texture()
+    t = texture.tex_coords
+    w, h = texture.width, texture.height
+    array = (GLfloat * 32)(
+         t[0][0], t[0][1], t[0][2], 1.,
+         x,       y,       z,       1.,
+         t[1][0], t[1][1], t[1][2], 1.,
+         x + w,   y,       z,       1.,
+         t[2][0], t[2][1], t[2][2], 1.,
+         x + w,   y + h,   z,       1.,
+         t[3][0], t[3][1], t[3][2], 1.,
+         x,       y + h,   z,       1.)
+
+    glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
+    glInterleavedArrays(GL_T4F_V4F, 0, array)
+    glDrawArrays(GL_QUADS, 0, 4)
+    glPopClientAttrib()
+
+The :py:meth:`~pyglet.image.Texture.blit` method does this.
+
+Use the :py:meth:`pyglet.image.Texture.create` method to create
+either a texture region from a larger power-2 sized texture,
+or a texture with the exact dimensions using  the
+``GL_texture_rectangle_ARB`` extension.
+
+Texture internal format
+^^^^^^^^^^^^^^^^^^^^^^^
+
+pyglet automatically selects an internal format for the texture based on the
+source image's `format` attribute.  The following table describes how it is
+selected.
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Format
+          - Internal format
+        * - Any format with 3 components
+          - ``GL_RGB``
+        * - Any format with 2 components
+          - ``GL_LUMINANCE_ALPHA``
+        * - ``"A"``
+          - ``GL_ALPHA``
+        * - ``"L"``
+          - ``GL_LUMINANCE``
+        * - ``"I"``
+          - ``GL_INTENSITY``
+        * - Any other format
+          - ``GL_RGBA``
+
+Note that this table does not imply any mapping between format components and
+their OpenGL counterparts.  For example, an image with format ``"RG"`` will use
+``GL_LUMINANCE_ALPHA`` as its internal format; the luminance channel will be
+averaged from the red and green components, and the alpha channel will be
+empty (maximal).
+
+Use the :py:meth:`pyglet.image.Texture.create` class method to create a texture
+with a specific internal format.
+
+Texture filtering
+^^^^^^^^^^^^^^^^^
+
+By default, all textures are created with smooth (``GL_LINEAR``) filtering.
+In some cases you may wish to have different filtered applied. Retro style
+pixel art games, for example, would require sharper textures. To achieve this,
+pas ``GL_NEAREST`` to the `min_filter` and `mag_filter` parameters when
+creating a texture. It is also possible to set the default filtering by
+setting the `default_min_filter` and `default_mag_filter` class attributes
+on the `Texture` class. This will cause all textures created internally by
+pyglet to use these values::
+
+    pyglet.image.Texture.default_min_filter = GL_LINEAR
+    pyglet.image.Texture.default_mag_filter = GL_LINEAR
+
+
+.. _OpenGL Programming Guide: http://www.glprogramming.com/red/
+
+Saving an image
+---------------
+
+Any image can be saved using the `save` method::
+
+    kitten.save('kitten.png')
+
+or, specifying a file-like object::
+
+    kitten_stream = open('kitten.png', 'wb')
+    kitten.save('kitten.png', file=kitten_stream)
+
+The following example shows how to grab a screenshot of your application
+window::
+
+    pyglet.image.get_buffer_manager().get_color_buffer().save('screenshot.png')
+
+Note that images can only be saved in the PNG format unless the Pillow library
+is installed.
diff --git a/doc/programming_guide/img/abstract_image.png b/doc/programming_guide/img/abstract_image.png
new file mode 100644
index 0000000..a111fd8
Binary files /dev/null and b/doc/programming_guide/img/abstract_image.png differ
diff --git a/doc/programming_guide/img/abstract_image.svg b/doc/programming_guide/img/abstract_image.svg
new file mode 100644
index 0000000..7c9973f
--- /dev/null
+++ b/doc/programming_guide/img/abstract_image.svg
@@ -0,0 +1,312 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="665.32001"
+   height="256.82999"
+   id="svg2891"
+   sodipodi:version="0.32"
+   inkscape:version="0.44.1"
+   sodipodi:docbase="/home/alex/projects/pyglet/doc/programming_guide"
+   sodipodi:docname="abstract_image.svg"
+   version="1.0">
+  <defs
+     id="defs2893" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.98994949"
+     inkscape:cx="339.04172"
+     inkscape:cy="88.968466"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     inkscape:window-width="854"
+     inkscape:window-height="599"
+     inkscape:window-x="1406"
+     inkscape:window-y="137"
+     width="665.32px"
+     height="256.83px" />
+  <metadata
+     id="metadata2896">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-24.30492,-35.1247)">
+    <rect
+       y="39.665314"
+       x="281.24579"
+       height="56.731632"
+       width="151.24506"
+       id="rect2804"
+       style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <text
+       sodipodi:linespacing="125%"
+       id="text2806"
+       y="53.845612"
+       x="356.74979"
+       style="font-size:12px;font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+       xml:space="preserve"><tspan
+         style="font-style:italic;font-family:Arial"
+         id="tspan2808"
+         y="53.845612"
+         x="356.74979"
+         sodipodi:role="line">AbstractImage</tspan></text>
+    <path
+       sodipodi:nodetypes="cc"
+       id="path2810"
+       d="M 281.04967,58.896404 L 432.51126,58.896404"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <g
+       id="g2950"
+       transform="translate(9.697133,4.04061)">
+      <rect
+         y="142.21065"
+         x="187.85818"
+         height="56.731632"
+         width="151.24506"
+         id="rect2869"
+         style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <text
+         sodipodi:linespacing="125%"
+         id="text2871"
+         y="156.39101"
+         x="263.36221"
+         style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+         xml:space="preserve"><tspan
+           style="font-style:normal;font-family:Arial"
+           id="tspan2873"
+           y="156.39101"
+           x="263.36221"
+           sodipodi:role="line">CompressedImageData</tspan></text>
+      <path
+         sodipodi:nodetypes="cc"
+         id="path2875"
+         d="M 187.66206,161.44174 L 339.12366,161.44174"
+         style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    </g>
+    <g
+       id="g2956"
+       transform="translate(3.030458,4.04061)">
+      <rect
+         style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+         id="rect2816"
+         width="151.24506"
+         height="56.731632"
+         x="25.358196"
+         y="142.21065" />
+      <text
+         xml:space="preserve"
+         style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+         x="100.86221"
+         y="156.39101"
+         id="text2818"
+         sodipodi:linespacing="125%"><tspan
+           sodipodi:role="line"
+           x="100.86221"
+           y="156.39101"
+           id="tspan2820"
+           style="font-family:Arial">ImageData</tspan></text>
+      <path
+         style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         d="M 25.162063,161.44174 L 176.62366,161.44174"
+         id="path2822"
+         sodipodi:nodetypes="cc" />
+    </g>
+    <g
+       id="g2962"
+       transform="translate(3.14954,4.04061)">
+      <rect
+         style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+         id="rect2853"
+         width="151.24506"
+         height="56.731632"
+         x="363.57245"
+         y="142.36798" />
+      <text
+         xml:space="preserve"
+         style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+         x="439.07651"
+         y="156.5484"
+         id="text2855"
+         sodipodi:linespacing="125%"><tspan
+           sodipodi:role="line"
+           x="439.07651"
+           y="156.5484"
+           id="tspan2857"
+           style="font-family:Arial">Texture</tspan></text>
+      <path
+         style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         d="M 363.37634,161.59911 L 514.83794,161.59911"
+         id="path2859"
+         sodipodi:nodetypes="cc" />
+    </g>
+    <g
+       id="g2968"
+       transform="translate(3.030517,4.04061)">
+      <rect
+         y="142.36798"
+         x="532.85815"
+         height="56.731632"
+         width="151.24506"
+         id="rect1872"
+         style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <text
+         sodipodi:linespacing="125%"
+         id="text2760"
+         y="156.5484"
+         x="608.36224"
+         style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+         xml:space="preserve"><tspan
+           style="font-family:Arial"
+           id="tspan2764"
+           y="156.5484"
+           x="608.36224"
+           sodipodi:role="line">ImageGrid</tspan></text>
+      <path
+         sodipodi:nodetypes="cc"
+         id="path2768"
+         d="M 532.66205,161.59911 L 684.12365,161.59911"
+         style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    </g>
+    <g
+       id="g2986"
+       transform="translate(3.030458,4.04061)">
+      <g
+         id="g2974">
+        <rect
+           style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+           id="rect2936"
+           width="151.24506"
+           height="56.731632"
+           x="25.001041"
+           y="227.72517" />
+        <text
+           xml:space="preserve"
+           style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+           x="100.50507"
+           y="241.90553"
+           id="text2938"
+           sodipodi:linespacing="125%"><tspan
+             sodipodi:role="line"
+             x="100.50507"
+             y="241.90553"
+             id="tspan2940"
+             style="font-style:normal;font-family:Arial">ImageDataRegion</tspan></text>
+        <path
+           style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           d="M 24.804917,246.95627 L 176.26652,246.95627"
+           id="path2942"
+           sodipodi:nodetypes="cc" />
+      </g>
+      <g
+         id="g2944"
+         transform="translate(-203.0458,-76.73749)">
+        <path
+           style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:bevel;stroke-opacity:1"
+           d="M 304.56099,275.55487 L 298.13276,285.15796 L 310.74818,285.15796 L 304.56099,275.55487 z "
+           id="path2946"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           d="M 304.47539,285.15939 L 304.47539,304.66243"
+           id="path2948"
+           sodipodi:nodetypes="cc" />
+      </g>
+    </g>
+    <g
+       id="g2996"
+       transform="translate(3.030458,4.04061)">
+      <g
+         id="g2980">
+        <rect
+           y="227.72517"
+           x="365.00104"
+           height="56.731632"
+           width="151.24506"
+           id="rect2845"
+           style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+        <text
+           sodipodi:linespacing="125%"
+           id="text2847"
+           y="241.90553"
+           x="440.50507"
+           style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+           xml:space="preserve"><tspan
+             style="font-style:normal;font-family:Arial"
+             id="tspan2849"
+             y="241.90553"
+             x="440.50507"
+             sodipodi:role="line">TextureRegion</tspan></text>
+        <path
+           sodipodi:nodetypes="cc"
+           id="path2851"
+           d="M 364.80492,246.95627 L 516.26652,246.95627"
+           style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+      </g>
+      <g
+         transform="translate(136.9542,-76.73749)"
+         id="g2830">
+        <path
+           sodipodi:nodetypes="cccc"
+           id="path2824"
+           d="M 304.56099,275.55487 L 298.13276,285.15796 L 310.74818,285.15796 L 304.56099,275.55487 z "
+           style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:bevel;stroke-opacity:1" />
+        <path
+           sodipodi:nodetypes="cc"
+           id="path2826"
+           d="M 304.47539,285.15939 L 304.47539,304.66243"
+           style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+      </g>
+    </g>
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:bevel;stroke-opacity:1"
+       d="M 355.25997,96.943974 L 348.83174,106.54706 L 361.44716,106.54706 L 355.25997,96.943974 z "
+       id="path2770"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 355.17437,106.54849 L 355.17437,123.55153"
+       id="path2772"
+       sodipodi:nodetypes="cc" />
+    <path
+       id="path2867"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 104.44899,146.09389 L 104.44899,123.77246 L 611.59184,123.79413 L 611.59184,146.45102"
+       sodipodi:nodetypes="cccc" />
+    <path
+       sodipodi:nodetypes="cc"
+       id="path3009"
+       d="M 442.67332,146.22422 L 442.67332,123.90279"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 274.48292,146.22422 L 274.48292,123.90279"
+       id="path2934"
+       sodipodi:nodetypes="cc" />
+  </g>
+</svg>
diff --git a/doc/programming_guide/img/buffer_image.png b/doc/programming_guide/img/buffer_image.png
new file mode 100644
index 0000000..34d0ae7
Binary files /dev/null and b/doc/programming_guide/img/buffer_image.png differ
diff --git a/doc/programming_guide/img/buffer_image.svg b/doc/programming_guide/img/buffer_image.svg
new file mode 100644
index 0000000..c2126c2
--- /dev/null
+++ b/doc/programming_guide/img/buffer_image.svg
@@ -0,0 +1,224 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="496.79001"
+   height="257.42999"
+   id="svg3011"
+   sodipodi:version="0.32"
+   inkscape:version="0.44.1"
+   sodipodi:docbase="/home/alex/projects/pyglet/doc/programming_guide"
+   sodipodi:docname="buffer_image.svg"
+   version="1.0">
+  <defs
+     id="defs3013" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.98994949"
+     inkscape:cx="354.01386"
+     inkscape:cy="111.62321"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     inkscape:window-width="854"
+     inkscape:window-height="599"
+     inkscape:window-x="1648"
+     inkscape:window-y="0"
+     height="257.43px"
+     width="496.79px" />
+  <metadata
+     id="metadata3016">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-28.0549,-20.49187)">
+    <rect
+       y="110.97659"
+       x="203.79425"
+       height="56.731632"
+       width="151.24506"
+       id="rect2804"
+       style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <text
+       sodipodi:linespacing="125%"
+       id="text2806"
+       y="125.15688"
+       x="279.30548"
+       style="font-size:12px;font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+       xml:space="preserve"><tspan
+         style="font-style:italic;font-family:Arial"
+         id="tspan2808"
+         y="125.15688"
+         x="279.30548"
+         sodipodi:role="line">BufferImage</tspan></text>
+    <path
+       sodipodi:nodetypes="cc"
+       id="path2810"
+       d="M 203.68599,130.20768 L 355.14758,130.20768"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <g
+       id="g2950"
+       transform="translate(14.10011,75.35186)">
+      <rect
+         y="142.21065"
+         x="187.85818"
+         height="56.731632"
+         width="151.24506"
+         id="rect2869"
+         style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <text
+         sodipodi:linespacing="125%"
+         id="text2871"
+         y="156.39101"
+         x="263.36221"
+         style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+         xml:space="preserve"><tspan
+           style="font-style:normal;font-family:Arial"
+           id="tspan2873"
+           y="156.39101"
+           x="263.36221"
+           sodipodi:role="line">DepthBufferImage</tspan></text>
+      <path
+         sodipodi:nodetypes="cc"
+         id="path2875"
+         d="M 187.66206,161.44174 L 339.12366,161.44174"
+         style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    </g>
+    <g
+       id="g2956"
+       transform="translate(7.433443,75.35186)">
+      <rect
+         style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+         id="rect2816"
+         width="151.24506"
+         height="56.731632"
+         x="25.358196"
+         y="142.21065" />
+      <text
+         xml:space="preserve"
+         style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+         x="100.86221"
+         y="156.39101"
+         id="text2818"
+         sodipodi:linespacing="125%"><tspan
+           sodipodi:role="line"
+           x="100.86221"
+           y="156.39101"
+           id="tspan2820"
+           style="font-family:Arial">ColorBufferImage</tspan></text>
+      <path
+         style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         d="M 25.162063,161.44174 L 176.62366,161.44174"
+         id="path2822"
+         sodipodi:nodetypes="cc" />
+    </g>
+    <g
+       id="g2962"
+       transform="translate(7.552523,75.35186)">
+      <rect
+         style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+         id="rect2853"
+         width="151.24506"
+         height="56.731632"
+         x="363.57245"
+         y="142.36798" />
+      <text
+         xml:space="preserve"
+         style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+         x="439.07651"
+         y="156.5484"
+         id="text2855"
+         sodipodi:linespacing="125%"><tspan
+           sodipodi:role="line"
+           x="439.07651"
+           y="156.5484"
+           id="tspan2857"
+           style="font-family:Arial">BufferImageMask</tspan></text>
+      <path
+         style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         d="M 363.37634,161.59911 L 514.83794,161.59911"
+         id="path2859"
+         sodipodi:nodetypes="cc" />
+    </g>
+    <g
+       id="g2974"
+       transform="translate(178.881,-203.7028)">
+      <rect
+         y="227.72517"
+         x="25.001041"
+         height="56.731632"
+         width="151.24506"
+         id="rect2936"
+         style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <text
+         sodipodi:linespacing="125%"
+         id="text2938"
+         y="241.90553"
+         x="100.50507"
+         style="font-size:12px;font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+         xml:space="preserve"><tspan
+           style="font-style:italic;font-family:Arial"
+           id="tspan2940"
+           y="241.90553"
+           x="100.50507"
+           sodipodi:role="line">AbstractImage</tspan></text>
+      <path
+         sodipodi:nodetypes="cc"
+         id="path2942"
+         d="M 24.804917,246.95627 L 176.26652,246.95627"
+         style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    </g>
+    <g
+       transform="translate(-25.02369,-194.0723)"
+       id="g2944">
+      <path
+         sodipodi:nodetypes="cccc"
+         id="path2946"
+         d="M 304.56099,275.55487 L 298.13276,285.15796 L 310.74818,285.15796 L 304.56099,275.55487 z "
+         style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:bevel;stroke-opacity:1" />
+      <path
+         sodipodi:nodetypes="cc"
+         id="path2948"
+         d="M 304.47539,285.15939 L 304.47539,304.66243"
+         style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    </g>
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:bevel;stroke-opacity:1"
+       d="M 279.5373,168.25525 L 273.10907,177.85834 L 285.72449,177.85834 L 279.5373,168.25525 z "
+       id="path2770"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 279.41678,177.85977 L 279.41678,217.54138"
+       id="path2772"
+       sodipodi:nodetypes="cc" />
+    <path
+       id="path2867"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 108.85197,217.40517 L 108.85197,195.08374 L 451.70911,195.10541 L 451.70911,217.7623"
+       sodipodi:nodetypes="cccc" />
+  </g>
+</svg>
diff --git a/doc/programming_guide/img/context_flow.png b/doc/programming_guide/img/context_flow.png
new file mode 100644
index 0000000..0a33531
Binary files /dev/null and b/doc/programming_guide/img/context_flow.png differ
diff --git a/doc/programming_guide/img/context_flow.svg b/doc/programming_guide/img/context_flow.svg
new file mode 100644
index 0000000..7c45b95
--- /dev/null
+++ b/doc/programming_guide/img/context_flow.svg
@@ -0,0 +1,475 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="605.39001"
+   height="286.20999"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.44.1"
+   sodipodi:docbase="/home/alex/projects/pyglet/doc"
+   sodipodi:docname="context_flow.svg"
+   version="1.0">
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient2794">
+      <stop
+         style="stop-color:#0038ff;stop-opacity:1;"
+         offset="0"
+         id="stop2796" />
+      <stop
+         style="stop-color:black;stop-opacity:1;"
+         offset="1"
+         id="stop2798" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient2786">
+      <stop
+         style="stop-color:#00d3ff;stop-opacity:1;"
+         offset="0"
+         id="stop2788" />
+      <stop
+         style="stop-color:#00d3ff;stop-opacity:0;"
+         offset="1"
+         id="stop2790" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2786"
+       id="linearGradient2792"
+       x1="193.03572"
+       y1="160.93361"
+       x2="193.03572"
+       y2="232.54076"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(121.2183,0)" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2786"
+       id="linearGradient2810"
+       gradientUnits="userSpaceOnUse"
+       x1="193.03572"
+       y1="160.93361"
+       x2="193.03572"
+       y2="232.54076" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2794"
+       id="linearGradient3009"
+       gradientUnits="userSpaceOnUse"
+       x1="177.85715"
+       y1="180.93361"
+       x2="232.18196"
+       y2="180.93361" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2794"
+       id="linearGradient3033"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(111.4285,0.714314)"
+       x1="177.85715"
+       y1="180.93361"
+       x2="232.18196"
+       y2="180.93361" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.98994949"
+     inkscape:cx="351.50082"
+     inkscape:cy="106.20906"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     inkscape:window-width="1007"
+     inkscape:window-height="745"
+     inkscape:window-x="1339"
+     inkscape:window-y="0"
+     width="605.39px"
+     height="286.21px" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-66.63839,-34.86932)">
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       x="223.32632"
+       y="55.861816"
+       id="text1872"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan1874"
+         x="223.32632"
+         y="55.861816">Display</tspan></text>
+    <g
+       id="g2855"
+       transform="translate(104.0406,-87.21464)">
+      <rect
+         style="opacity:1;fill:url(#linearGradient2810);fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:3.29999995;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="rect2808"
+         width="111.42859"
+         height="88.664246"
+         x="140"
+         y="156.55508"
+         rx="0"
+         ry="0" />
+      <path
+         style="fill:black;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         d="M 192.7346,245.21932 L 191.40012,258.79075 L 159.16317,258.79075 L 164.16317,263.07647 L 227.7346,263.07647 L 219.98597,258.6028 L 196.40012,258.6028 L 198.52981,244.85366 L 192.7346,245.21932 z "
+         id="path2812"
+         sodipodi:nodetypes="ccccccccc" />
+    </g>
+    <path
+       sodipodi:nodetypes="csscssccsscccccccssccc"
+       id="path2987"
+       d="M 563.48826,108.22286 C 555.64184,108.28292 549.09665,108.61506 543.20701,110.22286 C 535.35415,112.3666 528.89757,117.76661 525.42576,126.22286 C 525.06853,127.09297 524.7448,128.00717 524.42576,128.94161 C 524.15798,128.18286 523.87574,127.4383 523.58201,126.72286 C 520.1102,118.26661 513.65362,112.8666 505.80076,110.72286 C 497.9479,108.57912 488.90438,108.72286 477.20701,108.72286 L 477.20701,120.12911 C 488.90581,120.12911 497.42755,120.25604 502.80076,121.72286 C 508.17397,123.18968 510.5859,125.03173 513.05076,131.03536 C 517.52399,141.93074 518.47365,167.27736 518.61326,215.06661 L 510.05076,215.16036 L 518.64451,223.56661 L 525.39451,230.16036 C 525.39451,230.16036 530.36326,225.50411 530.36326,225.50411 L 541.83201,214.78536 L 530.39451,214.91036 C 530.53032,166.88917 531.47244,141.45837 535.95701,130.53536 C 538.42187,124.53174 540.8338,122.68968 546.20701,121.22286 C 551.58022,119.75604 560.10196,119.62911 571.80076,119.62911 L 571.80076,108.22286 C 568.87642,108.22286 566.10373,108.20284 563.48826,108.22286 z "
+       style="fill:#ff001c;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:0.69999999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="cccccccccc"
+       id="path2989"
+       d="M 499.08427,260.59118 C 488.18889,265.06441 486.77084,266.01407 438.98159,266.15368 L 438.88784,257.59118 L 430.48159,266.18493 L 423.88784,272.93493 C 423.88784,272.93493 428.54409,277.90368 428.54409,277.90368 L 439.26284,289.37243 L 439.13784,277.93493 C 487.15903,278.07074 488.66126,279.01286 499.58427,283.49743 C 499.58427,283.49743 499.08427,260.59118 499.08427,260.59118 z "
+       style="fill:#ff001c;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:0.69999999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#a7a7a7;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:4, 4;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 227.61204,249.43326 L 402.25489,100.14753"
+       id="path3011"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#a7a7a7;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:4, 4;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 290.46918,309.43326 L 439.7549,135.14754"
+       id="path3013"
+       sodipodi:nodetypes="cc" />
+    <path
+       sodipodi:nodetypes="cccccccccc"
+       id="path3066"
+       d="M 367.6557,265.59118 C 356.76032,270.06441 355.34227,271.01407 307.55302,271.15368 L 307.45927,262.59118 L 299.05302,271.18493 L 292.45927,277.93493 C 292.45927,277.93493 297.11552,282.90368 297.11552,282.90368 L 307.83427,294.37243 L 307.70927,282.93493 C 355.73046,283.07074 357.23269,284.01286 368.1557,288.49743 C 368.1557,288.49743 367.6557,265.59118 367.6557,265.59118 z "
+       style="fill:#ff001c;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:0.69999999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    <path
+       style="fill:#ff001c;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:0.69999999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 142.95162,107.5023 C 153.847,111.97553 155.26505,112.92519 203.05429,113.0648 L 203.14804,104.50229 L 211.55429,113.09605 L 218.14804,119.84605 C 218.14804,119.84605 213.49179,124.8148 213.49179,124.8148 L 202.77304,136.28355 L 202.89804,124.84605 C 154.87686,124.98186 153.37463,125.92398 142.45162,130.40855 C 142.45162,130.40855 142.95162,107.5023 142.95162,107.5023 z "
+       id="path2970"
+       sodipodi:nodetypes="cccccccccc" />
+    <g
+       id="g2859"
+       transform="translate(104.0406,-87.21464)">
+      <rect
+         ry="0"
+         rx="0"
+         y="156.55508"
+         x="261.21829"
+         height="88.664246"
+         width="111.42859"
+         id="rect1892"
+         style="opacity:1;fill:url(#linearGradient2792);fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:3.29999995;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <path
+         sodipodi:nodetypes="ccccccccc"
+         id="path2780"
+         d="M 313.95291,245.21932 L 312.61843,258.79075 L 280.38148,258.79075 L 285.38148,263.07647 L 348.95291,263.07647 L 341.20428,258.6028 L 317.61843,258.6028 L 319.74812,244.85366 L 313.95291,245.21932 z "
+         style="fill:black;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    </g>
+    <text
+       xml:space="preserve"
+       style="font-size:16px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       x="369.75491"
+       y="83.004684"
+       id="text1876"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan1878"
+         x="369.75491"
+         y="83.004684"
+         style="font-size:12px">Screen</tspan></text>
+    <g
+       transform="translate(55.46916,72.78536)"
+       id="g2991"
+       style="opacity:1">
+      <rect
+         y="178.07515"
+         x="172.73608"
+         height="58.083771"
+         width="62.124382"
+         id="rect2993"
+         style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <text
+         xml:space="preserve"
+         style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+         x="174.99997"
+         y="193.79076"
+         id="text2995"
+         sodipodi:linespacing="125%"><tspan
+           sodipodi:role="line"
+           id="tspan2997"
+           x="174.99997"
+           y="193.79076">Window</tspan></text>
+      <rect
+         y="178.07515"
+         x="172.86235"
+         height="4.4194174"
+         width="61.871845"
+         id="rect2999"
+         style="opacity:1;fill:url(#linearGradient3009);fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <g
+         id="g3001"
+         transform="translate(1.428571,5.714286)">
+        <path
+           style="fill:#8f2b2b;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:bevel;stroke-opacity:1"
+           d="M 188.48214,202.18361 L 202.58929,211.82647 L 203.21429,225.93361 L 193.30357,218.79075 L 188.48214,202.18361 z "
+           id="path3003"
+           sodipodi:nodetypes="ccccc" />
+        <path
+           style="fill:#cf0000;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:bevel;stroke-opacity:1"
+           d="M 203.03571,211.29076 L 213.92857,204.59433 L 201.96429,193.96932 L 188.21428,202.00503 L 203.03571,211.29076 z "
+           id="path3005"
+           sodipodi:nodetypes="ccccc" />
+        <path
+           style="fill:#995c5c;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:bevel;stroke-opacity:1"
+           d="M 213.48215,204.7729 L 211.07143,218.43361 L 203.39286,225.84432 L 203.30358,211.91576 L 213.48215,204.7729 z "
+           id="path3007"
+           sodipodi:nodetypes="ccccc" />
+      </g>
+    </g>
+    <g
+       id="g3025"
+       transform="matrix(0.603957,0,0,0.603957,230.6698,-8.032832)"
+       style="opacity:0.67613639">
+      <rect
+         style="fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="rect2814"
+         width="62.124382"
+         height="58.083771"
+         x="284.16458"
+         y="178.78946" />
+      <rect
+         style="fill:url(#linearGradient3033);fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="rect2816"
+         width="61.871845"
+         height="4.4194174"
+         x="284.29086"
+         y="178.78946" />
+      <g
+         transform="translate(112.8571,6.4286)"
+         id="g2828">
+        <path
+           sodipodi:nodetypes="ccccc"
+           id="path2820"
+           d="M 188.48214,202.18361 L 202.58929,211.82647 L 203.21429,225.93361 L 193.30357,218.79075 L 188.48214,202.18361 z "
+           style="fill:#8f2b2b;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:bevel;stroke-opacity:1" />
+        <path
+           sodipodi:nodetypes="ccccc"
+           id="path2822"
+           d="M 203.03571,211.29076 L 213.92857,204.59433 L 201.96429,193.96932 L 188.21428,202.00503 L 203.03571,211.29076 z "
+           style="fill:#cf0000;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:bevel;stroke-opacity:1" />
+        <path
+           sodipodi:nodetypes="ccccc"
+           id="path2824"
+           d="M 213.48215,204.7729 L 211.07143,218.43361 L 203.39286,225.84432 L 203.30358,211.91576 L 213.48215,204.7729 z "
+           style="fill:#995c5c;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:bevel;stroke-opacity:1" />
+      </g>
+    </g>
+    <rect
+       style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:0.69999999;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:5.6, 5.6;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect2826"
+       width="275.71429"
+       height="149.28571"
+       x="218.32632"
+       y="42.29039" />
+    <g
+       id="g2940"
+       transform="translate(59.39775,-75.42893)">
+      <rect
+         ry="9.2857141"
+         rx="9.2857141"
+         y="305.93362"
+         x="413.57135"
+         height="87.14286"
+         width="107.14286"
+         id="rect2863"
+         style="fill:#fffebb;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <text
+         sodipodi:linespacing="125%"
+         id="text1880"
+         y="320.21936"
+         x="421.4285"
+         style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+         xml:space="preserve"><tspan
+           y="320.21936"
+           x="421.4285"
+           id="tspan1882"
+           sodipodi:role="line">Complete Config</tspan></text>
+      <text
+         sodipodi:linespacing="125%"
+         id="text2865"
+         y="336.64792"
+         x="421.4285"
+         style="font-size:8px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+         xml:space="preserve"><tspan
+           y="336.64792"
+           x="421.4285"
+           id="tspan2867"
+           sodipodi:role="line">double_buffer = True</tspan><tspan
+           id="tspan2869"
+           y="346.64792"
+           x="421.4285"
+           sodipodi:role="line">red_size = 8</tspan><tspan
+           id="tspan2871"
+           y="356.64792"
+           x="421.4285"
+           sodipodi:role="line">green_size = 8</tspan><tspan
+           id="tspan2873"
+           y="366.64792"
+           x="421.4285"
+           sodipodi:role="line">blue_size = 8</tspan><tspan
+           id="tspan2875"
+           y="376.64792"
+           x="421.4285"
+           sodipodi:role="line">aux_buffers = 4</tspan></text>
+    </g>
+    <g
+       id="g3085">
+      <rect
+         style="fill:#fffebb;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+         id="rect2890"
+         width="107.14286"
+         height="87.14286"
+         x="560.42706"
+         y="68.300529"
+         rx="9.2857141"
+         ry="9.2857141" />
+      <text
+         xml:space="preserve"
+         style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+         x="567.2793"
+         y="82.407692"
+         id="text2892"
+         sodipodi:linespacing="125%"><tspan
+           sodipodi:role="line"
+           id="tspan2894"
+           x="567.2793"
+           y="82.407692">Template Config</tspan></text>
+      <text
+         xml:space="preserve"
+         style="font-size:8px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+         x="568.28418"
+         y="99.014824"
+         id="text2896"
+         sodipodi:linespacing="125%"><tspan
+           sodipodi:role="line"
+           id="tspan2898"
+           x="568.28418"
+           y="99.014824">double_buffer = True</tspan><tspan
+           sodipodi:role="line"
+           x="568.28418"
+           y="109.01482"
+           id="tspan2900">red_size = </tspan><tspan
+           sodipodi:role="line"
+           x="568.28418"
+           y="119.01482"
+           id="tspan2902">green_size =</tspan><tspan
+           sodipodi:role="line"
+           x="568.28418"
+           y="129.01482"
+           id="tspan2904">blue_size =</tspan><tspan
+           sodipodi:role="line"
+           x="568.28418"
+           y="139.01482"
+           id="tspan2906">aux_buffers = </tspan></text>
+    </g>
+    <g
+       id="g2961"
+       transform="translate(139.7549,-80.07179)">
+      <rect
+         style="opacity:1;fill:#e3e3e3;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+         id="rect2957"
+         width="64.285721"
+         height="63.571434"
+         x="218.57143"
+         y="318.79074" />
+      <rect
+         style="opacity:1;fill:#e3e3e3;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+         id="rect2959"
+         width="64.285721"
+         height="63.571434"
+         x="212.14285"
+         y="325.2193" />
+      <rect
+         y="333.07645"
+         x="205.71428"
+         height="63.571434"
+         width="64.285721"
+         id="rect2955"
+         style="opacity:1;fill:#e3e3e3;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <text
+         sodipodi:linespacing="125%"
+         id="text1884"
+         y="392.36221"
+         x="207.85715"
+         style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+         xml:space="preserve"><tspan
+           y="392.36221"
+           x="207.85715"
+           id="tspan1886"
+           sodipodi:role="line">Context</tspan></text>
+    </g>
+    <g
+       id="g3068"
+       transform="translate(86.89775,-88.64322)">
+      <path
+         style="opacity:1;fill:#8f8f8f;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+         d="M -4.28125,186.63393 L -15.71875,196.63393 L -15.71875,233.79018 L 64.28125,233.79018 L 75.71875,223.79018 L 75.71875,186.63393 L -4.28125,186.63393 z "
+         id="path3062"
+         sodipodi:nodetypes="ccccccc" />
+      <path
+         style="opacity:1;fill:#9f9f9f;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+         d="M 64.33584,196.63393 L 64.33584,233.79018 L 75.71875,223.79018 L 75.71875,186.63393 L 64.33584,196.63393 z "
+         id="path3064"
+         sodipodi:nodetypes="ccccc" />
+      <path
+         sodipodi:nodetypes="ccsccc"
+         id="rect3055"
+         d="M -4.28125,186.63393 L -15.71875,196.63393 C -15.71875,196.63393 -15.71875,197.36161 -15.71875,197.36161 C -15.71875,197.36161 64.28125,197.36161 64.28125,197.36161 L 75.71875,186.63393 L -4.28125,186.63393 z "
+         style="opacity:1;fill:#a7a7a7;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <text
+         sodipodi:linespacing="125%"
+         id="text2951"
+         y="227.36218"
+         x="-11.428574"
+         style="font-size:8px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+         xml:space="preserve"><tspan
+           style="font-size:12px"
+           y="227.36218"
+           x="-11.428574"
+           id="tspan2953"
+           sodipodi:role="line">Platform</tspan></text>
+    </g>
+  </g>
+</svg>
diff --git a/doc/programming_guide/img/cursor_mac_crosshair.png b/doc/programming_guide/img/cursor_mac_crosshair.png
new file mode 100644
index 0000000..a5b8d0e
Binary files /dev/null and b/doc/programming_guide/img/cursor_mac_crosshair.png differ
diff --git a/doc/programming_guide/img/cursor_mac_default.png b/doc/programming_guide/img/cursor_mac_default.png
new file mode 100644
index 0000000..87a1c3f
Binary files /dev/null and b/doc/programming_guide/img/cursor_mac_default.png differ
diff --git a/doc/programming_guide/img/cursor_mac_hand.png b/doc/programming_guide/img/cursor_mac_hand.png
new file mode 100644
index 0000000..af7f68d
Binary files /dev/null and b/doc/programming_guide/img/cursor_mac_hand.png differ
diff --git a/doc/programming_guide/img/cursor_mac_no.png b/doc/programming_guide/img/cursor_mac_no.png
new file mode 100644
index 0000000..941ad1e
Binary files /dev/null and b/doc/programming_guide/img/cursor_mac_no.png differ
diff --git a/doc/programming_guide/img/cursor_mac_size_down.png b/doc/programming_guide/img/cursor_mac_size_down.png
new file mode 100644
index 0000000..15d455a
Binary files /dev/null and b/doc/programming_guide/img/cursor_mac_size_down.png differ
diff --git a/doc/programming_guide/img/cursor_mac_size_left.png b/doc/programming_guide/img/cursor_mac_size_left.png
new file mode 100644
index 0000000..5c15ed5
Binary files /dev/null and b/doc/programming_guide/img/cursor_mac_size_left.png differ
diff --git a/doc/programming_guide/img/cursor_mac_size_left_right.png b/doc/programming_guide/img/cursor_mac_size_left_right.png
new file mode 100644
index 0000000..6dc2359
Binary files /dev/null and b/doc/programming_guide/img/cursor_mac_size_left_right.png differ
diff --git a/doc/programming_guide/img/cursor_mac_size_right.png b/doc/programming_guide/img/cursor_mac_size_right.png
new file mode 100644
index 0000000..d3f24c8
Binary files /dev/null and b/doc/programming_guide/img/cursor_mac_size_right.png differ
diff --git a/doc/programming_guide/img/cursor_mac_size_up.png b/doc/programming_guide/img/cursor_mac_size_up.png
new file mode 100644
index 0000000..f01d5d5
Binary files /dev/null and b/doc/programming_guide/img/cursor_mac_size_up.png differ
diff --git a/doc/programming_guide/img/cursor_mac_size_up_down.png b/doc/programming_guide/img/cursor_mac_size_up_down.png
new file mode 100644
index 0000000..ee588f0
Binary files /dev/null and b/doc/programming_guide/img/cursor_mac_size_up_down.png differ
diff --git a/doc/programming_guide/img/cursor_mac_text.png b/doc/programming_guide/img/cursor_mac_text.png
new file mode 100644
index 0000000..56f7199
Binary files /dev/null and b/doc/programming_guide/img/cursor_mac_text.png differ
diff --git a/doc/programming_guide/img/cursor_mac_wait.png b/doc/programming_guide/img/cursor_mac_wait.png
new file mode 100644
index 0000000..c1915e1
Binary files /dev/null and b/doc/programming_guide/img/cursor_mac_wait.png differ
diff --git a/doc/programming_guide/img/cursor_win_crosshair.png b/doc/programming_guide/img/cursor_win_crosshair.png
new file mode 100644
index 0000000..d716419
Binary files /dev/null and b/doc/programming_guide/img/cursor_win_crosshair.png differ
diff --git a/doc/programming_guide/img/cursor_win_default.png b/doc/programming_guide/img/cursor_win_default.png
new file mode 100644
index 0000000..757c8ac
Binary files /dev/null and b/doc/programming_guide/img/cursor_win_default.png differ
diff --git a/doc/programming_guide/img/cursor_win_hand.png b/doc/programming_guide/img/cursor_win_hand.png
new file mode 100644
index 0000000..fd80510
Binary files /dev/null and b/doc/programming_guide/img/cursor_win_hand.png differ
diff --git a/doc/programming_guide/img/cursor_win_help.png b/doc/programming_guide/img/cursor_win_help.png
new file mode 100644
index 0000000..4794fa6
Binary files /dev/null and b/doc/programming_guide/img/cursor_win_help.png differ
diff --git a/doc/programming_guide/img/cursor_win_no.png b/doc/programming_guide/img/cursor_win_no.png
new file mode 100644
index 0000000..9b60894
Binary files /dev/null and b/doc/programming_guide/img/cursor_win_no.png differ
diff --git a/doc/programming_guide/img/cursor_win_size.png b/doc/programming_guide/img/cursor_win_size.png
new file mode 100644
index 0000000..b787637
Binary files /dev/null and b/doc/programming_guide/img/cursor_win_size.png differ
diff --git a/doc/programming_guide/img/cursor_win_size_left_right.png b/doc/programming_guide/img/cursor_win_size_left_right.png
new file mode 100644
index 0000000..764fb36
Binary files /dev/null and b/doc/programming_guide/img/cursor_win_size_left_right.png differ
diff --git a/doc/programming_guide/img/cursor_win_size_nesw.png b/doc/programming_guide/img/cursor_win_size_nesw.png
new file mode 100644
index 0000000..1780345
Binary files /dev/null and b/doc/programming_guide/img/cursor_win_size_nesw.png differ
diff --git a/doc/programming_guide/img/cursor_win_size_nwse.png b/doc/programming_guide/img/cursor_win_size_nwse.png
new file mode 100644
index 0000000..a694fa0
Binary files /dev/null and b/doc/programming_guide/img/cursor_win_size_nwse.png differ
diff --git a/doc/programming_guide/img/cursor_win_size_up_down.png b/doc/programming_guide/img/cursor_win_size_up_down.png
new file mode 100644
index 0000000..51d4b99
Binary files /dev/null and b/doc/programming_guide/img/cursor_win_size_up_down.png differ
diff --git a/doc/programming_guide/img/cursor_win_text.png b/doc/programming_guide/img/cursor_win_text.png
new file mode 100644
index 0000000..a415450
Binary files /dev/null and b/doc/programming_guide/img/cursor_win_text.png differ
diff --git a/doc/programming_guide/img/cursor_win_wait.png b/doc/programming_guide/img/cursor_win_wait.png
new file mode 100644
index 0000000..c7a65f8
Binary files /dev/null and b/doc/programming_guide/img/cursor_win_wait.png differ
diff --git a/doc/programming_guide/img/cursor_win_wait_arrow.png b/doc/programming_guide/img/cursor_win_wait_arrow.png
new file mode 100644
index 0000000..13d7923
Binary files /dev/null and b/doc/programming_guide/img/cursor_win_wait_arrow.png differ
diff --git a/doc/programming_guide/img/explosion.png b/doc/programming_guide/img/explosion.png
new file mode 100644
index 0000000..fcd56dd
Binary files /dev/null and b/doc/programming_guide/img/explosion.png differ
diff --git a/doc/programming_guide/img/font_metrics.png b/doc/programming_guide/img/font_metrics.png
new file mode 100644
index 0000000..19f0c79
Binary files /dev/null and b/doc/programming_guide/img/font_metrics.png differ
diff --git a/doc/programming_guide/img/font_metrics.svg b/doc/programming_guide/img/font_metrics.svg
new file mode 100644
index 0000000..5f91f13
--- /dev/null
+++ b/doc/programming_guide/img/font_metrics.svg
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="310.17999"
+   height="156.97"
+   id="svg3318"
+   sodipodi:version="0.32"
+   inkscape:version="0.44.1"
+   sodipodi:docbase="/home/alex/projects/pyglet/doc/programming_guide"
+   sodipodi:docname="font_metrics.svg"
+   version="1.0">
+  <defs
+     id="defs3320" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.7"
+     inkscape:cx="287.07337"
+     inkscape:cy="-19.453896"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     inkscape:window-width="854"
+     inkscape:window-height="599"
+     inkscape:window-x="1515"
+     inkscape:window-y="327"
+     height="156.97px"
+     width="310.18px" />
+  <metadata
+     id="metadata3323">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-77.57767,-241.0055)">
+    <text
+       xml:space="preserve"
+       style="font-size:144px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       x="217.55766"
+       y="355.78131"
+       id="text3326"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3328"
+         x="217.55766"
+         y="355.78131">dog</tspan></text>
+    <path
+       id="path3332"
+       d="M 93.179139,355.89789 L 385.11323,355.89789"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:red;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:red;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 93.179139,391.25323 L 385.11323,391.25323"
+       id="path3349" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:red;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 93.179139,245.79126 L 385.11323,245.79126"
+       id="path3330" />
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       x="360.78546"
+       y="352.62234"
+       id="text3336"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3338"
+         x="360.78546"
+         y="352.62234">baseline</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       x="-319.26764"
+       y="88.927971"
+       id="text3343"
+       sodipodi:linespacing="125%"
+       transform="matrix(0,-1,1,0,0,0)"><tspan
+         sodipodi:role="line"
+         id="tspan3345"
+         x="-319.26764"
+         y="88.927971">ascent</tspan></text>
+    <text
+       transform="matrix(0,-1,1,0,0,0)"
+       sodipodi:linespacing="125%"
+       id="text3109"
+       y="89.024651"
+       x="-394.67065"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       xml:space="preserve"><tspan
+         y="89.024651"
+         x="-394.67065"
+         id="tspan3111"
+         sodipodi:role="line">descent</tspan></text>
+    <path
+       id="path3347"
+       style="fill:red;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
+       d="M 97.380412,353.54404 L 92.031782,344.61243 L 97.365092,353.52872 L 103.04437,344.66772 L 97.360922,353.55916 L 97.360922,248.00565 L 103.04437,256.89709 L 97.365092,248.03609 L 92.031782,256.95238 L 97.380412,248.02077"
+       sodipodi:nodetypes="cccccccccc" />
+    <path
+       sodipodi:nodetypes="cccccccccc"
+       d="M 97.380412,389.40446 L 92.031782,380.47285 L 97.365092,389.38914 L 103.04437,380.52814 L 97.360922,389.41958 L 97.360922,358.61736 L 103.04437,367.5088 L 97.365092,358.6478 L 92.031782,367.56409 L 97.380412,358.63248"
+       style="fill:red;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
+       id="path3101" />
+  </g>
+</svg>
diff --git a/doc/programming_guide/img/image_classes.png b/doc/programming_guide/img/image_classes.png
new file mode 100644
index 0000000..c885fc7
Binary files /dev/null and b/doc/programming_guide/img/image_classes.png differ
diff --git a/doc/programming_guide/img/image_classes.svg b/doc/programming_guide/img/image_classes.svg
new file mode 100644
index 0000000..efe96a2
--- /dev/null
+++ b/doc/programming_guide/img/image_classes.svg
@@ -0,0 +1,1836 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   style="fill-opacity:1; color-rendering:auto; color-interpolation:auto; text-rendering:auto; stroke:black; stroke-linecap:square; stroke-miterlimit:10; shape-rendering:auto; stroke-opacity:1; fill:black; stroke-dasharray:none; font-weight:normal; stroke-width:1; font-family:'Dialog'; font-style:normal; stroke-linejoin:miter; font-size:12; stroke-dashoffset:0; image-rendering:auto;"
+   width="936"
+   height="576"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.44.1"
+   sodipodi:docname="image_classes.svg"
+   sodipodi:docbase="/home/alex/projects/pyglet/doc">
+  <metadata
+     id="metadata656">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview
+     inkscape:window-height="638"
+     inkscape:window-width="858"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     guidetolerance="10.0"
+     gridtolerance="10.0"
+     objecttolerance="10.0"
+     borderopacity="1.0"
+     bordercolor="#666666"
+     pagecolor="#ffffff"
+     id="base"
+     inkscape:zoom="0.70833333"
+     inkscape:cx="468"
+     inkscape:cy="173.64706"
+     inkscape:window-x="1280"
+     inkscape:window-y="328"
+     inkscape:current-layer="svg2" />
+<!--Generated by the Batik Graphics2D SVG Generator-->  <defs
+     id="genericDefs">
+    <defs
+       id="defs1">
+      <clipPath
+         id="clipPath1"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path9"
+           d="M-3 -3 L937 -3 L937 577 L-3 577 L-3 -3 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath2"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path12"
+           d="M0 0 L0 14 L930 14 L930 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath3"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path15"
+           d="M-3 -3 L197 -3 L197 97 L-3 97 L-3 -3 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath4"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path18"
+           d="M0 0 L0 83 L190 83 L190 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath5"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path21"
+           d="M0 0 L0 14 L190 14 L190 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath6"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path24"
+           d="M-3 -3 L217 -3 L217 187 L-3 187 L-3 -3 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath7"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path27"
+           d="M0 0 L0 159 L210 159 L210 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath8"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path30"
+           d="M0 0 L0 28 L210 28 L210 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath9"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path33"
+           d="M-3 -3 L127 -3 L127 47 L-3 47 L-3 -3 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath10"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path36"
+           d="M0 0 L0 33 L120 33 L120 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath11"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path39"
+           d="M0 0 L0 14 L120 14 L120 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath12"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path42"
+           d="M-3 -3 L227 -3 L227 87 L-3 87 L-3 -3 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath13"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path45"
+           d="M0 0 L0 73 L220 73 L220 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath14"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path48"
+           d="M0 0 L0 14 L220 14 L220 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath15"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path51"
+           d="M-3 -3 L117 -3 L117 47 L-3 47 L-3 -3 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath16"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path54"
+           d="M0 0 L0 33 L110 33 L110 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath17"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path57"
+           d="M0 0 L0 14 L110 14 L110 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath18"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path60"
+           d="M-3 -3 L177 -3 L177 57 L-3 57 L-3 -3 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath19"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path63"
+           d="M0 0 L0 43 L170 43 L170 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath20"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path66"
+           d="M0 0 L0 14 L170 14 L170 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath21"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path69"
+           d="M-3 -3 L167 -3 L167 107 L-3 107 L-3 -3 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath22"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path72"
+           d="M0 0 L0 79 L160 79 L160 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath23"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path75"
+           d="M0 0 L0 28 L160 28 L160 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath24"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path78"
+           d="M0 0 L0 19 L120 19 L120 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath25"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path81"
+           d="M0 0 L0 28 L120 28 L120 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath26"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path84"
+           d="M-3 -3 L177 -3 L177 77 L-3 77 L-3 -3 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath27"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path87"
+           d="M0 0 L0 49 L170 49 L170 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath28"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path90"
+           d="M0 0 L0 28 L170 28 L170 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath29"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path93"
+           d="M-3 -3 L187 -3 L187 77 L-3 77 L-3 -3 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath30"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path96"
+           d="M0 0 L0 63 L180 63 L180 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath31"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path99"
+           d="M0 0 L0 14 L180 14 L180 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath32"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path102"
+           d="M-3 -3 L187 -3 L187 57 L-3 57 L-3 -3 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath33"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path105"
+           d="M0 0 L0 43 L180 43 L180 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath34"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path108"
+           d="M-3 -3 L107 -3 L107 47 L-3 47 L-3 -3 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath35"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path111"
+           d="M0 0 L0 33 L100 33 L100 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath36"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path114"
+           d="M0 0 L0 14 L100 14 L100 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath37"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path117"
+           d="M-3 -3 L87 -3 L87 47 L-3 47 L-3 -3 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath38"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path120"
+           d="M0 0 L0 33 L80 33 L80 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath39"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path123"
+           d="M0 0 L0 14 L80 14 L80 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath40"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path126"
+           d="M0 0 L474 0 L474 143 L0 143 L0 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath41"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path129"
+           d="M0 0 L104 0 L104 133 L0 133 L0 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath42"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path132"
+           d="M0 0 L104 0 L104 143 L0 143 L0 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath43"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path135"
+           d="M0 0 L334 0 L334 263 L0 263 L0 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath44"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path138"
+           d="M0 0 L154 0 L154 143 L0 143 L0 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath45"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path141"
+           d="M0 0 L284 0 L284 143 L0 143 L0 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath46"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path144"
+           d="M0 0 L234 0 L234 143 L0 143 L0 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath47"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path147"
+           d="M0 0 L394 0 L394 143 L0 143 L0 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath48"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path150"
+           d="M0 0 L304 0 L304 143 L0 143 L0 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath49"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path153"
+           d="M0 0 L274 0 L274 193 L0 193 L0 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath50"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path156"
+           d="M0 0 L174 0 L174 133 L0 133 L0 0 Z" />
+      </clipPath>
+      <clipPath
+         id="clipPath51"
+         clipPathUnits="userSpaceOnUse">
+        <path
+           id="path159"
+           d="M0 0 L104 0 L104 203 L0 203 L0 0 Z" />
+      </clipPath>
+    </defs>
+  </defs>
+  <g
+     id="g161"
+     style="fill:white;stroke:white">
+    <rect
+       id="rect163"
+       height="576"
+       style="stroke:none"
+       width="936"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g165"
+     style="font-size:11px;fill:#7acff5;stroke:#7acff5;font-family:sans-serif"
+     transform="translate(2,2)">
+    <rect
+       id="rect167"
+       height="20"
+       style="stroke:none"
+       width="185"
+       y="0"
+       x="0" />
+    <rect
+       id="rect169"
+       height="549"
+       style="stroke:none"
+       width="929"
+       y="20"
+       x="0" />
+  </g>
+  <g
+     id="g171"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(2,2)">
+    <rect
+       id="rect173"
+       height="20"
+       style="fill:none"
+       width="185"
+       y="0"
+       x="0" />
+    <rect
+       id="rect175"
+       height="549"
+       style="fill:none"
+       width="929"
+       y="20"
+       x="0" />
+  </g>
+  <g
+     id="g177"
+     transform="translate(2,22)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text179"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="431">pyglet.image</text>
+  </g>
+  <g
+     id="g181"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(22,272)">
+    <rect
+       id="rect183"
+       height="90"
+       style="stroke:none"
+       width="190"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g185"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(22,272)">
+    <rect
+       id="rect187"
+       height="90"
+       style="fill:none"
+       width="190"
+       y="0"
+       x="0" />
+    <line
+       id="line189"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="190"
+       x1="0" />
+  </g>
+  <g
+     id="g191"
+     transform="translate(22,286)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text193"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+data</text>
+    <text
+       id="text195"
+       xml:space="preserve"
+       style="stroke:none"
+       y="27"
+       x="2">+format</text>
+    <text
+       id="text197"
+       xml:space="preserve"
+       style="stroke:none"
+       y="41"
+       x="2">+pitch</text>
+    <line
+       id="line199"
+       y2="46"
+       style="fill:none;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0"
+       y1="46"
+       x2="190"
+       x1="0" />
+    <text
+       id="text201"
+       xml:space="preserve"
+       style="stroke:none"
+       y="59"
+       x="2">+set_mipmap_image(level, image)</text>
+  </g>
+  <g
+     id="g203"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(22,272)">
+    <text
+       id="text205"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="65">ImageData</text>
+  </g>
+  <g
+     id="g207"
+     style="font-size:11px;font-weight:bold;fill:#ffc800;stroke:#ffc800;font-family:sans-serif"
+     transform="translate(392,52)">
+    <rect
+       id="rect209"
+       height="180"
+       style="stroke:none"
+       width="210"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g211"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(392,52)">
+    <rect
+       id="rect213"
+       height="180"
+       style="fill:none"
+       width="210"
+       y="0"
+       x="0" />
+    <line
+       id="line215"
+       y2="28"
+       style="fill:none"
+       y1="28"
+       x2="210"
+       x1="0" />
+  </g>
+  <g
+     id="g217"
+     transform="translate(392,80)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text219"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+image_data</text>
+    <text
+       id="text221"
+       xml:space="preserve"
+       style="stroke:none"
+       y="27"
+       x="2">+texture</text>
+    <text
+       id="text223"
+       xml:space="preserve"
+       style="stroke:none"
+       y="41"
+       x="2">+mipmapped_texture</text>
+    <text
+       id="text225"
+       xml:space="preserve"
+       style="stroke:none"
+       y="55"
+       x="2">+width</text>
+    <text
+       id="text227"
+       xml:space="preserve"
+       style="stroke:none"
+       y="69"
+       x="2">+height</text>
+    <line
+       id="line229"
+       y2="74"
+       style="fill:none;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0"
+       y1="74"
+       x2="210"
+       x1="0" />
+    <text
+       id="text231"
+       xml:space="preserve"
+       style="stroke:none"
+       y="87"
+       x="2">+get_region(x, y, width, height)</text>
+    <text
+       id="text233"
+       xml:space="preserve"
+       style="stroke:none"
+       y="101"
+       x="2">+save(filename, file, encoder)</text>
+    <text
+       id="text235"
+       xml:space="preserve"
+       style="stroke:none"
+       y="115"
+       x="2">+blit(x, y, z)</text>
+    <text
+       id="text237"
+       xml:space="preserve"
+       style="stroke:none"
+       y="129"
+       x="2">+blit_into(source, x, y, z)</text>
+    <text
+       id="text239"
+       xml:space="preserve"
+       style="stroke:none"
+       y="143"
+       x="2">+blit_to_texture(target, level, x, y, z)</text>
+  </g>
+  <g
+     id="g241"
+     transform="translate(392,52)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text243"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="63">&lt;&lt;Interface&gt;&gt;</text>
+    <text
+       id="text245"
+       xml:space="preserve"
+       style="font-weight:bold;stroke:none"
+       y="25"
+       x="65">AbstractImage</text>
+  </g>
+  <g
+     id="g247"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(62,392)">
+    <rect
+       id="rect249"
+       height="40"
+       style="stroke:none"
+       width="120"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g251"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(62,392)">
+    <rect
+       id="rect253"
+       height="40"
+       style="fill:none"
+       width="120"
+       y="0"
+       x="0" />
+    <line
+       id="line255"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="120"
+       x1="0" />
+  </g>
+  <g
+     id="g257"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(62,392)">
+    <text
+       id="text259"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="11">ImageDataRegion</text>
+  </g>
+  <g
+     id="g261"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(422,272)">
+    <rect
+       id="rect263"
+       height="80"
+       style="stroke:none"
+       width="220"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g265"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(422,272)">
+    <rect
+       id="rect267"
+       height="80"
+       style="fill:none"
+       width="220"
+       y="0"
+       x="0" />
+    <line
+       id="line269"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="220"
+       x1="0" />
+  </g>
+  <g
+     id="g271"
+     transform="translate(422,286)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text273"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+tex_coords</text>
+    <text
+       id="text275"
+       xml:space="preserve"
+       style="stroke:none"
+       y="27"
+       x="2">+target</text>
+    <text
+       id="text277"
+       xml:space="preserve"
+       style="stroke:none"
+       y="41"
+       x="2">+level</text>
+    <line
+       id="line279"
+       y2="46"
+       style="fill:none;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0"
+       y1="46"
+       x2="220"
+       x1="0" />
+    <text
+       id="text281"
+       xml:space="preserve"
+       style="stroke:none"
+       y="59"
+       x="2">+create_for_size(target, width, height)</text>
+    <line
+       id="line283"
+       y2="61"
+       style="fill:none"
+       y1="61"
+       x2="208"
+       x1="2" />
+  </g>
+  <g
+     id="g285"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(422,272)">
+    <text
+       id="text287"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="88">Texture</text>
+  </g>
+  <g
+     id="g289"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(522,392)">
+    <rect
+       id="rect291"
+       height="40"
+       style="stroke:none"
+       width="110"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g293"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(522,392)">
+    <rect
+       id="rect295"
+       height="40"
+       style="fill:none"
+       width="110"
+       y="0"
+       x="0" />
+    <line
+       id="line297"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="110"
+       x1="0" />
+  </g>
+  <g
+     id="g299"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(522,392)">
+    <text
+       id="text301"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="14">TextureRegion</text>
+  </g>
+  <g
+     id="g303"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(742,512)">
+    <rect
+       id="rect305"
+       height="50"
+       style="stroke:none"
+       width="170"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g307"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(742,512)">
+    <rect
+       id="rect309"
+       height="50"
+       style="fill:none"
+       width="170"
+       y="0"
+       x="0" />
+    <line
+       id="line311"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="170"
+       x1="0" />
+  </g>
+  <g
+     id="g313"
+     transform="translate(742,526)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text315"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+create_for_images(images)</text>
+    <line
+       id="line317"
+       y2="15"
+       style="fill:none"
+       y1="15"
+       x2="156"
+       x1="2" />
+    <text
+       id="text319"
+       xml:space="preserve"
+       style="stroke:none"
+       y="27"
+       x="2">+create_for_image_grid(grid)</text>
+    <line
+       id="line321"
+       y2="29"
+       style="fill:none"
+       y1="29"
+       x2="162"
+       x1="2" />
+  </g>
+  <g
+     id="g323"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(742,512)">
+    <text
+       id="text325"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="55">Texture3D</text>
+  </g>
+  <g
+     id="g327"
+     style="font-size:11px;font-weight:bold;fill:#ffc800;stroke:#ffc800;font-family:sans-serif"
+     transform="translate(682,132)">
+    <rect
+       id="rect329"
+       height="100"
+       style="stroke:none"
+       width="160"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g331"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(682,132)">
+    <rect
+       id="rect333"
+       height="100"
+       style="fill:none"
+       width="160"
+       y="0"
+       x="0" />
+    <line
+       id="line335"
+       y2="28"
+       style="fill:none"
+       y1="28"
+       x2="160"
+       x1="0" />
+  </g>
+  <g
+     id="g337"
+     transform="translate(682,160)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text339"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+texture_sequence</text>
+    <line
+       id="line341"
+       y2="18"
+       style="fill:none;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0"
+       y1="18"
+       x2="160"
+       x1="0" />
+    <text
+       id="text343"
+       xml:space="preserve"
+       style="stroke:none"
+       y="31"
+       x="2">+__len__()</text>
+    <text
+       id="text345"
+       xml:space="preserve"
+       style="stroke:none"
+       y="45"
+       x="2">+__getitem__(slice)</text>
+    <text
+       id="text347"
+       xml:space="preserve"
+       style="stroke:none"
+       y="59"
+       x="2">+__setitem__(slice, image)</text>
+  </g>
+  <g
+     id="g349"
+     transform="translate(682,132)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text351"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="38">&lt;&lt;Interface&gt;&gt;</text>
+    <text
+       id="text353"
+       xml:space="preserve"
+       style="font-weight:bold;stroke:none"
+       y="25"
+       x="14">AbstractImageSequence</text>
+  </g>
+  <g
+     id="g355"
+     style="font-size:11px;font-weight:bold;fill:#ffc800;stroke:#ffc800;font-family:sans-serif"
+     transform="translate(762,272)">
+    <rect
+       id="rect357"
+       height="40"
+       style="stroke:none"
+       width="120"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g359"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(762,272)">
+    <rect
+       id="rect361"
+       height="40"
+       style="fill:none"
+       width="120"
+       y="0"
+       x="0" />
+    <line
+       id="line363"
+       y2="28"
+       style="fill:none"
+       y1="28"
+       x2="120"
+       x1="0" />
+  </g>
+  <g
+     id="g365"
+     transform="translate(762,272)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text367"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="18">&lt;&lt;Interface&gt;&gt;</text>
+    <text
+       id="text369"
+       xml:space="preserve"
+       style="font-weight:bold;stroke:none"
+       y="25"
+       x="13">TextureSequence</text>
+  </g>
+  <g
+     id="g371"
+     style="font-size:11px;font-weight:bold;fill:#ffc800;stroke:#ffc800;font-family:sans-serif"
+     transform="translate(742,342)">
+    <rect
+       id="rect373"
+       height="70"
+       style="stroke:none"
+       width="170"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g375"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(742,342)">
+    <rect
+       id="rect377"
+       height="70"
+       style="fill:none"
+       width="170"
+       y="0"
+       x="0" />
+    <line
+       id="line379"
+       y2="28"
+       style="fill:none"
+       y1="28"
+       x2="170"
+       x1="0" />
+  </g>
+  <g
+     id="g381"
+     transform="translate(742,370)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text383"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+item_width</text>
+    <text
+       id="text385"
+       xml:space="preserve"
+       style="stroke:none"
+       y="27"
+       x="2">+item_height</text>
+  </g>
+  <g
+     id="g387"
+     transform="translate(742,342)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text389"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="43">&lt;&lt;Interface&gt;&gt;</text>
+    <text
+       id="text391"
+       xml:space="preserve"
+       style="font-weight:bold;stroke:none"
+       y="25"
+       x="14">UniformTextureSequence</text>
+  </g>
+  <g
+     id="g393"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(232,272)">
+    <rect
+       id="rect395"
+       height="70"
+       style="stroke:none"
+       width="180"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g397"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(232,272)">
+    <rect
+       id="rect399"
+       height="70"
+       style="fill:none"
+       width="180"
+       y="0"
+       x="0" />
+    <line
+       id="line401"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="180"
+       x1="0" />
+  </g>
+  <g
+     id="g403"
+     transform="translate(232,286)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text405"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+data</text>
+    <text
+       id="text407"
+       xml:space="preserve"
+       style="stroke:none"
+       y="27"
+       x="2">+gl_format</text>
+    <line
+       id="line409"
+       y2="32"
+       style="fill:none;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0"
+       y1="32"
+       x2="180"
+       x1="0" />
+    <text
+       id="text411"
+       xml:space="preserve"
+       style="stroke:none"
+       y="45"
+       x="2">+set_mipmap_data(level, data)</text>
+  </g>
+  <g
+     id="g413"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(232,272)">
+    <text
+       id="text415"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="26">CompressedImageData</text>
+  </g>
+  <g
+     id="g417"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(322,392)">
+    <rect
+       id="rect419"
+       height="50"
+       style="stroke:none"
+       width="180"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g421"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(322,392)">
+    <rect
+       id="rect423"
+       height="50"
+       style="fill:none"
+       width="180"
+       y="0"
+       x="0" />
+    <line
+       id="line425"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="180"
+       x1="0" />
+  </g>
+  <g
+     id="g427"
+     transform="translate(322,406)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text429"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+create_for_image(image)</text>
+    <line
+       id="line431"
+       y2="15"
+       style="fill:none"
+       y1="15"
+       x2="144"
+       x1="2" />
+    <text
+       id="text433"
+       xml:space="preserve"
+       style="stroke:none"
+       y="27"
+       x="2">+blit_tiled(x, y, z, width, height)</text>
+  </g>
+  <g
+     id="g435"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(322,392)">
+    <text
+       id="text437"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="46">TileableTexture</text>
+  </g>
+  <g
+     id="g439"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(202,392)">
+    <rect
+       id="rect441"
+       height="40"
+       style="stroke:none"
+       width="100"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g443"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(202,392)">
+    <rect
+       id="rect445"
+       height="40"
+       style="fill:none"
+       width="100"
+       y="0"
+       x="0" />
+    <line
+       id="line447"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="100"
+       x1="0" />
+  </g>
+  <g
+     id="g449"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(202,392)">
+    <text
+       id="text451"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="11">DepthTexture</text>
+  </g>
+  <g
+     id="g453"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(662,272)">
+    <rect
+       id="rect455"
+       height="40"
+       style="stroke:none"
+       width="80"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g457"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(662,272)">
+    <rect
+       id="rect459"
+       height="40"
+       style="fill:none"
+       width="80"
+       y="0"
+       x="0" />
+    <line
+       id="line461"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="80"
+       x1="0" />
+  </g>
+  <g
+     id="g463"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(662,272)">
+    <text
+       id="text465"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="11">ImageGrid</text>
+  </g>
+  <g
+     id="g467"
+     style="font-size:11px;font-weight:bold;fill:white;stroke:white;font-family:sans-serif"
+     transform="translate(682,442)">
+    <rect
+       id="rect469"
+       height="40"
+       style="stroke:none"
+       width="110"
+       y="0"
+       x="0" />
+  </g>
+  <g
+     id="g471"
+     style="font-size:11px;font-weight:bold;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:0;font-family:sans-serif"
+     transform="translate(682,442)">
+    <rect
+       id="rect473"
+       height="40"
+       style="fill:none"
+       width="110"
+       y="0"
+       x="0" />
+    <line
+       id="line475"
+       y2="14"
+       style="fill:none"
+       y1="14"
+       x2="110"
+       x1="0" />
+  </g>
+  <g
+     id="g477"
+     transform="translate(682,456)"
+     style="font-size:11px;font-family:sans-serif">
+    <text
+       id="text479"
+       xml:space="preserve"
+       style="stroke:none"
+       y="13"
+       x="2">+get(row, column)</text>
+  </g>
+  <g
+     id="g481"
+     style="font-size:11px;font-weight:bold;font-family:sans-serif"
+     transform="translate(682,442)">
+    <text
+       id="text483"
+       xml:space="preserve"
+       style="stroke:none"
+       y="11"
+       x="21">TextureGrid</text>
+  </g>
+  <g
+     id="g485"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(72,182)">
+    <line
+       id="line487"
+       y2="70"
+       style="fill:none"
+       y1="50"
+       x2="420"
+       x1="420" />
+    <line
+       id="line489"
+       y2="70"
+       style="fill:none"
+       y1="70"
+       x2="50"
+       x1="420" />
+    <line
+       id="line491"
+       y2="89"
+       style="fill:none"
+       y1="70"
+       x2="50"
+       x1="50" />
+    <polygon
+       id="polygon493"
+       points="420,50 414,62 426,62 420,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon495"
+       points="420,50 414,62 426,62 420,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g497"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(72,312)">
+    <line
+       id="line499"
+       y2="79"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="50" />
+    <polygon
+       id="polygon501"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon503"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g505"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(442,182)">
+    <line
+       id="line507"
+       y2="89"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="50" />
+    <polygon
+       id="polygon509"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon511"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g513"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(492,302)">
+    <line
+       id="line515"
+       y2="89"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="50" />
+    <polygon
+       id="polygon517"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon519"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g521"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(492,302)">
+    <line
+       id="line523"
+       y2="70"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="50" />
+    <line
+       id="line525"
+       y2="70"
+       style="fill:none"
+       y1="70"
+       x2="150"
+       x1="50" />
+    <line
+       id="line527"
+       y2="190"
+       style="fill:none"
+       y1="70"
+       x2="150"
+       x1="150" />
+    <line
+       id="line529"
+       y2="190"
+       style="fill:none"
+       y1="190"
+       x2="280"
+       x1="150" />
+    <line
+       id="line531"
+       y2="209"
+       style="fill:none"
+       y1="190"
+       x2="280"
+       x1="280" />
+    <polygon
+       id="polygon533"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon535"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g537"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(722,182)">
+    <line
+       id="line539"
+       y2="70"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="50" />
+    <line
+       id="line541"
+       y2="70"
+       style="fill:none"
+       y1="70"
+       x2="100"
+       x1="50" />
+    <line
+       id="line543"
+       y2="89"
+       style="fill:none"
+       y1="70"
+       x2="100"
+       x1="100" />
+    <polygon
+       id="polygon545"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon547"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g549"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(772,262)">
+    <line
+       id="line551"
+       y2="79"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="50" />
+    <polygon
+       id="polygon553"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon555"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g557"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(262,182)">
+    <line
+       id="line559"
+       y2="70"
+       style="fill:none"
+       y1="50"
+       x2="230"
+       x1="230" />
+    <line
+       id="line561"
+       y2="70"
+       style="fill:none"
+       y1="70"
+       x2="50"
+       x1="230" />
+    <line
+       id="line563"
+       y2="89"
+       style="fill:none"
+       y1="70"
+       x2="50"
+       x1="50" />
+    <polygon
+       id="polygon565"
+       points="230,50 224,62 236,62 230,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon567"
+       points="230,50 224,62 236,62 230,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g569"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(362,302)">
+    <line
+       id="line571"
+       y2="70"
+       style="fill:none"
+       y1="50"
+       x2="180"
+       x1="180" />
+    <line
+       id="line573"
+       y2="70"
+       style="fill:none"
+       y1="70"
+       x2="50"
+       x1="180" />
+    <line
+       id="line575"
+       y2="89"
+       style="fill:none"
+       y1="70"
+       x2="50"
+       x1="50" />
+    <polygon
+       id="polygon577"
+       points="180,50 174,62 186,62 180,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon579"
+       points="180,50 174,62 186,62 180,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g581"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(202,302)">
+    <line
+       id="line583"
+       y2="70"
+       style="fill:none"
+       y1="50"
+       x2="340"
+       x1="340" />
+    <line
+       id="line585"
+       y2="70"
+       style="fill:none"
+       y1="70"
+       x2="50"
+       x1="340" />
+    <line
+       id="line587"
+       y2="89"
+       style="fill:none"
+       y1="70"
+       x2="50"
+       x1="50" />
+    <polygon
+       id="polygon589"
+       points="340,50 334,62 346,62 340,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon591"
+       points="340,50 334,62 346,62 340,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g593"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(442,182)">
+    <line
+       id="line595"
+       y2="70"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="50" />
+    <line
+       id="line597"
+       y2="70"
+       style="fill:none"
+       y1="70"
+       x2="250"
+       x1="50" />
+    <line
+       id="line599"
+       y2="89"
+       style="fill:none"
+       y1="70"
+       x2="250"
+       x1="250" />
+    <polygon
+       id="polygon601"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon603"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g605"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(672,182)">
+    <line
+       id="line607"
+       y2="70"
+       style="fill:none"
+       y1="50"
+       x2="100"
+       x1="100" />
+    <line
+       id="line609"
+       y2="70"
+       style="fill:none"
+       y1="70"
+       x2="50"
+       x1="100" />
+    <line
+       id="line611"
+       y2="89"
+       style="fill:none"
+       y1="70"
+       x2="50"
+       x1="50" />
+    <polygon
+       id="polygon613"
+       points="100,50 94,62 106,62 100,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon615"
+       points="100,50 94,62 106,62 100,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g617"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(492,302)">
+    <line
+       id="line619"
+       y2="70"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="50" />
+    <line
+       id="line621"
+       y2="70"
+       style="fill:none"
+       y1="70"
+       x2="220"
+       x1="50" />
+    <line
+       id="line623"
+       y2="139"
+       style="fill:none"
+       y1="70"
+       x2="220"
+       x1="220" />
+    <polygon
+       id="polygon625"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon627"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g629"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(702,362)">
+    <line
+       id="line631"
+       y2="70"
+       style="fill:none"
+       y1="50"
+       x2="120"
+       x1="120" />
+    <line
+       id="line633"
+       y2="70"
+       style="fill:none"
+       y1="70"
+       x2="50"
+       x1="120" />
+    <line
+       id="line635"
+       y2="79"
+       style="fill:none"
+       y1="70"
+       x2="50"
+       x1="50" />
+    <polygon
+       id="polygon637"
+       points="120,50 114,62 126,62 120,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon639"
+       points="120,50 114,62 126,62 120,50 "
+       style="fill:none" />
+  </g>
+  <g
+     id="g641"
+     style="font-size:11px;stroke-linecap:butt;stroke-linejoin:round;font-family:sans-serif"
+     transform="translate(772,362)">
+    <line
+       id="line643"
+       y2="149"
+       style="fill:none"
+       y1="50"
+       x2="50"
+       x1="50" />
+    <polygon
+       id="polygon645"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:white;stroke:none" />
+    <polygon
+       id="polygon647"
+       points="50,50 44,62 56,62 50,50 "
+       style="fill:none" />
+  </g>
+</svg>
diff --git a/doc/programming_guide/img/image_grid.png b/doc/programming_guide/img/image_grid.png
new file mode 100644
index 0000000..b8d6a01
Binary files /dev/null and b/doc/programming_guide/img/image_grid.png differ
diff --git a/doc/programming_guide/img/image_grid.svg b/doc/programming_guide/img/image_grid.svg
new file mode 100644
index 0000000..9017b22
--- /dev/null
+++ b/doc/programming_guide/img/image_grid.svg
@@ -0,0 +1,549 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="472.92999"
+   height="386.32001"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.44.1"
+   sodipodi:docbase="/home/alex/projects/pyglet/doc/programming_guide"
+   sodipodi:docname="image_grid.svg"
+   version="1.0">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.98994949"
+     inkscape:cx="417.4342"
+     inkscape:cy="139.48792"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     inkscape:window-width="1230"
+     inkscape:window-height="972"
+     inkscape:window-x="1280"
+     inkscape:window-y="0"
+     width="472.93px"
+     height="386.32px" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-36.37564,-92.57646)">
+    <path
+       sodipodi:nodetypes="cc"
+       id="path3028"
+       d="M 447.9079,273.24979 L 347.9079,293.96408"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#83e9ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:6, 6;stroke-dashoffset:0;stroke-opacity:1" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#83e9ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:6, 6;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 462.19361,96.106932 L 349.33647,128.96408"
+       id="path2982"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#ff8383;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:6, 6;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 335.05076,442.5355 L 270.76505,293.96407"
+       id="path2984" />
+    <path
+       id="path2778"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 118.8919,129.16686 L 118.8919,294.74777 M 42.926407,128.66686 L 42.926407,294.24777 M 194.8574,129.16686 L 194.8574,294.74777 M 270.82291,129.16686 L 270.82291,294.74777 M 346.78841,129.16686 L 346.78841,294.74777"
+       sodipodi:nodetypes="cccccccccc" />
+    <rect
+       style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect1872"
+       width="305.11325"
+       height="167.00949"
+       x="43.436558"
+       y="128.74844" />
+    <path
+       id="path2788"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 41.926407,187.67155 L 347.75391,187.67155 M 41.926407,240.95969 L 347.75391,240.95969 M 42.926407,294.24777 L 348.75391,294.24777"
+       sodipodi:nodetypes="cccccc" />
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       x="115.05077"
+       y="272.90207"
+       id="text2828"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2830"
+         x="115.05077"
+         y="272.90207">0</tspan><tspan
+         sodipodi:role="line"
+         x="115.05077"
+         y="287.90207"
+         id="tspan2832">(0, 0)</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       x="115.05077"
+       y="220.75922"
+       id="text2858"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2860"
+         x="115.05077"
+         y="220.75922">4</tspan><tspan
+         sodipodi:role="line"
+         x="115.05077"
+         y="235.75922"
+         id="tspan2862">(1, 0)</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       x="115.05077"
+       y="167.18779"
+       id="text2888"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2890"
+         x="115.05077"
+         y="167.18779">8</tspan><tspan
+         sodipodi:role="line"
+         x="115.05077"
+         y="182.18779"
+         id="tspan2892">(2, 0)</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text2954"
+       y="272.90207"
+       x="190.76505"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       xml:space="preserve"><tspan
+         y="272.90207"
+         x="190.76505"
+         id="tspan2956"
+         sodipodi:role="line">1</tspan><tspan
+         id="tspan2958"
+         y="287.90207"
+         x="190.76505"
+         sodipodi:role="line">(0, 1)</tspan></text>
+    <rect
+       y="387.53549"
+       x="176.47934"
+       height="54.285713"
+       width="76.428574"
+       id="rect2992"
+       style="opacity:1;fill:#ffc3c3;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <rect
+       style="opacity:1;fill:#c3fff9;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect2950"
+       width="76.428574"
+       height="54.285713"
+       x="372.9079"
+       y="219.67836" />
+    <rect
+       style="opacity:1;fill:#ffc3c3;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect2994"
+       width="76.428574"
+       height="54.285713"
+       x="189.33649"
+       y="326.8212" />
+    <rect
+       y="157.53549"
+       x="380.05075"
+       height="54.285713"
+       width="76.428574"
+       id="rect2948"
+       style="opacity:1;fill:#c3fff9;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <rect
+       style="opacity:1;fill:#ffc3c3;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect2996"
+       width="76.428574"
+       height="54.285713"
+       x="259.33649"
+       y="387.53549" />
+    <text
+       sodipodi:linespacing="125%"
+       id="text2998"
+       y="421.47351"
+       x="249.33647"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:#908484;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       xml:space="preserve"><tspan
+         y="421.47351"
+         x="249.33647"
+         id="tspan3000"
+         sodipodi:role="line">1</tspan><tspan
+         id="tspan3002"
+         y="436.47351"
+         x="249.33647"
+         sodipodi:role="line">(0, 1)</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:#908484;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       x="445.76505"
+       y="253.61638"
+       id="text2834"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2836"
+         x="445.76505"
+         y="253.61638">3</tspan><tspan
+         sodipodi:role="line"
+         x="445.76505"
+         y="268.61638"
+         id="tspan2838">(0, 3)</tspan></text>
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#ff8383;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:6, 6;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 176.18347,441.43869 L 119.33647,294.67836"
+       id="path2978"
+       sodipodi:nodetypes="cc" />
+    <text
+       sodipodi:linespacing="125%"
+       id="text2960"
+       y="220.75922"
+       x="190.76505"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       xml:space="preserve"><tspan
+         y="220.75922"
+         x="190.76505"
+         id="tspan2962"
+         sodipodi:role="line">5</tspan><tspan
+         id="tspan2964"
+         y="235.75922"
+         x="190.76505"
+         sodipodi:role="line">(1, 1)</tspan></text>
+    <rect
+       y="326.8212"
+       x="272.1936"
+       height="54.285713"
+       width="76.428574"
+       id="rect3004"
+       style="opacity:1;fill:#ffc3c3;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <rect
+       style="opacity:1;fill:#c3fff9;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect2946"
+       width="76.428574"
+       height="54.285713"
+       x="386.47934"
+       y="96.106918" />
+    <text
+       sodipodi:linespacing="125%"
+       id="text3006"
+       y="360.04495"
+       x="262.9079"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:#908484;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       xml:space="preserve"><tspan
+         y="360.04495"
+         x="262.9079"
+         id="tspan3008"
+         sodipodi:role="line">5</tspan><tspan
+         id="tspan3010"
+         y="375.04495"
+         x="262.9079"
+         sodipodi:role="line">(1, 1)</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:#908484;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       x="453.62219"
+       y="190.75925"
+       id="text2864"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2866"
+         x="453.62219"
+         y="190.75925">7</tspan><tspan
+         sodipodi:role="line"
+         x="453.62219"
+         y="205.75925"
+         id="tspan2868">(1, 3)</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       x="190.76505"
+       y="167.18779"
+       id="text2894"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2896"
+         x="190.76505"
+         y="167.18779">9</tspan><tspan
+         sodipodi:role="line"
+         x="190.76505"
+         y="182.18779"
+         id="tspan2898">(2, 1)</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text2966"
+       y="272.90207"
+       x="266.47931"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       xml:space="preserve"><tspan
+         y="272.90207"
+         x="266.47931"
+         id="tspan2968"
+         sodipodi:role="line">2</tspan><tspan
+         id="tspan2970"
+         y="287.90207"
+         x="266.47931"
+         sodipodi:role="line">(0, 2)</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text3012"
+       y="421.47351"
+       x="332.9079"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:#908484;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       xml:space="preserve"><tspan
+         y="421.47351"
+         x="332.9079"
+         id="tspan3014"
+         sodipodi:role="line">2</tspan><tspan
+         id="tspan3016"
+         y="436.47351"
+         x="332.9079"
+         sodipodi:role="line">(0, 2)</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text2972"
+       y="220.75922"
+       x="266.47931"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       xml:space="preserve"><tspan
+         y="220.75922"
+         x="266.47931"
+         id="tspan2974"
+         sodipodi:role="line">6</tspan><tspan
+         id="tspan2976"
+         y="235.75922"
+         x="266.47931"
+         sodipodi:role="line">(1, 2)</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text3018"
+       y="360.75922"
+       x="344.33646"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:#908484;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       xml:space="preserve"><tspan
+         y="360.75922"
+         x="344.33646"
+         id="tspan3020"
+         sodipodi:role="line">6</tspan><tspan
+         id="tspan3022"
+         y="375.75922"
+         x="344.33646"
+         sodipodi:role="line">(1, 2)</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:#908484;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       x="458.62216"
+       y="130.04494"
+       id="text2870"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2872"
+         x="458.62216"
+         y="130.04494">11</tspan><tspan
+         sodipodi:role="line"
+         x="458.62216"
+         y="145.04494"
+         id="tspan2874">(2, 3)</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       x="266.47931"
+       y="167.18779"
+       id="text2900"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2902"
+         x="266.47931"
+         y="167.18779">10</tspan><tspan
+         sodipodi:role="line"
+         x="266.47931"
+         y="182.18779"
+         id="tspan2904">(2, 2)</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       x="342.9079"
+       y="272.90207"
+       id="text2846"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2848"
+         x="342.9079"
+         y="272.90207">3</tspan><tspan
+         sodipodi:role="line"
+         x="342.9079"
+         y="287.90207"
+         id="tspan2850">(0, 3)</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       x="342.9079"
+       y="220.75922"
+       id="text2876"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2878"
+         x="342.9079"
+         y="220.75922">7</tspan><tspan
+         sodipodi:role="line"
+         x="342.9079"
+         y="235.75922"
+         id="tspan2880">(1, 3)</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       x="342.9079"
+       y="167.18779"
+       id="text2906"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2908"
+         x="342.9079"
+         y="167.18779">11</tspan><tspan
+         sodipodi:role="line"
+         x="342.9079"
+         y="182.18779"
+         id="tspan2910">(2, 3)</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text3060"
+       y="458.96408"
+       x="335.76505"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       xml:space="preserve"><tspan
+         y="458.96408"
+         x="335.76505"
+         id="tspan3062"
+         sodipodi:role="line">[1:11]</tspan><tspan
+         id="tspan3064"
+         y="473.96408"
+         x="335.76505"
+         sodipodi:role="line">[(0,1):(2,3)]</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       x="507.1936"
+       y="256.10693"
+       id="text2986"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2988"
+         x="507.1936"
+         y="256.10693">[3:16]</tspan><tspan
+         sodipodi:role="line"
+         x="507.1936"
+         y="271.10693"
+         id="tspan2990">[(0,3):(3,4)]</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text3036"
+       y="398.42834"
+       x="179.51505"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       xml:space="preserve"><tspan
+         y="398.42834"
+         x="179.51505"
+         id="tspan3038"
+         sodipodi:role="line">0</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text3040"
+       y="397.17834"
+       x="262.1936"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       xml:space="preserve"><tspan
+         y="397.17834"
+         x="262.1936"
+         id="tspan3042"
+         sodipodi:role="line">1</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text3044"
+       y="336.1069"
+       x="273.97931"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       xml:space="preserve"><tspan
+         y="336.1069"
+         x="273.97931"
+         id="tspan3046"
+         sodipodi:role="line">3</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text3048"
+       y="336.1069"
+       x="190.58647"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       xml:space="preserve"><tspan
+         y="336.1069"
+         x="190.58647"
+         id="tspan3050"
+         sodipodi:role="line">2</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text3052"
+       y="228.78549"
+       x="374.51505"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       xml:space="preserve"><tspan
+         y="228.78549"
+         x="374.51505"
+         id="tspan3054"
+         sodipodi:role="line">0</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text3056"
+       y="166.64262"
+       x="381.47931"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       xml:space="preserve"><tspan
+         y="166.64262"
+         x="381.47931"
+         id="tspan3058"
+         sodipodi:role="line">1</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       x="387.9079"
+       y="104.85692"
+       id="text3032"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3034"
+         x="387.9079"
+         y="104.85692">2</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Gentium"
+       x="43.622192"
+       y="121.10693"
+       id="text3066"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3068"
+         x="43.622192"
+         y="121.10693">[:]</tspan></text>
+  </g>
+</svg>
diff --git a/doc/programming_guide/img/image_sequence.png b/doc/programming_guide/img/image_sequence.png
new file mode 100644
index 0000000..1b13b20
Binary files /dev/null and b/doc/programming_guide/img/image_sequence.png differ
diff --git a/doc/programming_guide/img/image_sequence.svg b/doc/programming_guide/img/image_sequence.svg
new file mode 100644
index 0000000..eb55d8c
--- /dev/null
+++ b/doc/programming_guide/img/image_sequence.svg
@@ -0,0 +1,247 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="408.10001"
+   height="360.47"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.44.1"
+   sodipodi:docbase="/home/alex/projects/pyglet/doc/programming_guide"
+   sodipodi:docname="image_sequence.svg"
+   version="1.0">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.98994949"
+     inkscape:cx="321.60127"
+     inkscape:cy="181.37274"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     inkscape:window-width="1136"
+     inkscape:window-height="829"
+     inkscape:window-x="1366"
+     inkscape:window-y="0"
+     height="360.47px"
+     width="408.1px" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-64.85068,-111.8622)">
+    <rect
+       style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect2869"
+       width="151.24506"
+       height="56.731632"
+       x="232.08742"
+       y="222.98875" />
+    <rect
+       y="222.98875"
+       x="69.587425"
+       height="56.731632"
+       width="151.24506"
+       id="rect2816"
+       style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <rect
+       y="308.5033"
+       x="232.08742"
+       height="56.731632"
+       width="151.24506"
+       id="rect2845"
+       style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <rect
+       y="413.14615"
+       x="149.94456"
+       height="56.731632"
+       width="151.24506"
+       id="rect2853"
+       style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <rect
+       style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect1872"
+       width="151.24506"
+       height="56.731632"
+       x="319.23029"
+       y="413.14615" />
+    <rect
+       y="116.40279"
+       x="143.15884"
+       height="56.731632"
+       width="151.24506"
+       id="rect2804"
+       style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <text
+       sodipodi:linespacing="125%"
+       id="text2806"
+       y="130.58311"
+       x="218.66284"
+       style="font-size:12px;font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+       xml:space="preserve"><tspan
+         style="font-style:italic;font-family:Arial"
+         id="tspan2808"
+         y="130.58311"
+         x="218.66284"
+         sodipodi:role="line">AbstractImageSequence</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+       x="307.59146"
+       y="237.16907"
+       id="text2871"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         x="307.59146"
+         y="237.16907"
+         id="tspan2873"
+         style="font-style:italic;font-family:Arial">TextureSequence</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text2818"
+       y="237.16907"
+       x="145.09145"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+       xml:space="preserve"><tspan
+         style="font-family:Arial"
+         id="tspan2820"
+         y="237.16907"
+         x="145.09145"
+         sodipodi:role="line">ImageGrid</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text2847"
+       y="322.68362"
+       x="307.59146"
+       style="font-size:12px;font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+       xml:space="preserve"><tspan
+         style="font-style:italic;font-family:Arial"
+         id="tspan2849"
+         y="322.68362"
+         x="307.59146"
+         sodipodi:role="line">UniformTextureSequence</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text2855"
+       y="427.32648"
+       x="225.44859"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+       xml:space="preserve"><tspan
+         style="font-family:Arial"
+         id="tspan2857"
+         y="427.32648"
+         x="225.44859"
+         sodipodi:role="line">TextureGrid</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+       x="394.73431"
+       y="427.32648"
+       id="text2760"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         x="394.73431"
+         y="427.32648"
+         id="tspan2764"
+         style="font-family:Arial">Texture3D</tspan></text>
+    <path
+       sodipodi:nodetypes="cc"
+       id="path2810"
+       d="M 142.96272,135.63389 L 294.42432,135.63389"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 231.89129,242.21984 L 383.35289,242.21984"
+       id="path2875"
+       sodipodi:nodetypes="cc" />
+    <path
+       sodipodi:nodetypes="cc"
+       id="path2822"
+       d="M 69.39129,242.21984 L 220.85289,242.21984"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="cc"
+       id="path2851"
+       d="M 231.89129,327.73437 L 383.35289,327.73437"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="cc"
+       id="path2859"
+       d="M 149.74843,432.37723 L 301.21003,432.37723"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 319.03414,432.37723 L 470.49574,432.37723"
+       id="path2768"
+       sodipodi:nodetypes="cc" />
+    <g
+       id="g2830"
+       transform="translate(4.04061,4.04061)">
+      <path
+         style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:bevel;stroke-opacity:1"
+         d="M 304.56099,275.55487 L 298.13276,285.15796 L 310.74818,285.15796 L 304.56099,275.55487 z "
+         id="path2824"
+         sodipodi:nodetypes="cccc" />
+      <path
+         style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         d="M 304.47539,285.15939 L 304.47539,304.66243"
+         id="path2826"
+         sodipodi:nodetypes="cc" />
+    </g>
+    <path
+       sodipodi:nodetypes="cccc"
+       id="path2863"
+       d="M 308.6016,365.11001 L 302.17337,374.7131 L 314.78879,374.7131 L 308.6016,365.11001 z "
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:bevel;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="cc"
+       id="path2865"
+       d="M 308.516,374.71453 L 308.516,389.93185"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="cc"
+       id="path2867"
+       d="M 264.7549,412.47422 L 264.7549,390.15279 L 357.79061,390.15279 L 357.79061,413.1885"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:bevel;stroke-opacity:1"
+       d="M 217.17303,173.68146 L 210.7448,183.28455 L 223.36022,183.28455 L 217.17303,173.68146 z "
+       id="path2770"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 217.08743,183.28598 L 217.08743,200.28902"
+       id="path2772"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 173.32633,222.83138 L 173.32633,200.50995 L 266.36204,200.50995 L 266.36204,223.54566"
+       id="path2828"
+       sodipodi:nodetypes="cc" />
+  </g>
+</svg>
diff --git a/doc/programming_guide/img/mouse_coordinates.png b/doc/programming_guide/img/mouse_coordinates.png
new file mode 100644
index 0000000..7b777d8
Binary files /dev/null and b/doc/programming_guide/img/mouse_coordinates.png differ
diff --git a/doc/programming_guide/img/mouse_coordinates.svg b/doc/programming_guide/img/mouse_coordinates.svg
new file mode 100644
index 0000000..9c5b4d2
--- /dev/null
+++ b/doc/programming_guide/img/mouse_coordinates.svg
@@ -0,0 +1,209 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="205.7"
+   height="195.10001"
+   id="svg1912"
+   sodipodi:version="0.32"
+   inkscape:version="0.44.1"
+   sodipodi:docbase="/home/alex/projects/pyglet/doc/programming_guide"
+   sodipodi:docname="mouse_coordinates.svg"
+   version="1.0">
+  <defs
+     id="defs1914">
+    <linearGradient
+       id="linearGradient2794">
+      <stop
+         style="stop-color:#0038ff;stop-opacity:1;"
+         offset="0"
+         id="stop2796" />
+      <stop
+         style="stop-color:black;stop-opacity:1;"
+         offset="1"
+         id="stop2798" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient2786">
+      <stop
+         style="stop-color:#00d3ff;stop-opacity:1;"
+         offset="0"
+         id="stop2788" />
+      <stop
+         style="stop-color:#00d3ff;stop-opacity:0;"
+         offset="1"
+         id="stop2790" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2786"
+       id="linearGradient1981"
+       gradientUnits="userSpaceOnUse"
+       x1="193.03572"
+       y1="160.93361"
+       x2="193.03572"
+       y2="232.54076" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2794"
+       id="linearGradient2060"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.76163,0,0,1.76163,-60.69281,353.5698)"
+       x1="177.85715"
+       y1="180.93361"
+       x2="232.18196"
+       y2="180.93361" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="330.26658"
+     inkscape:cy="123.68582"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     inkscape:window-width="1230"
+     inkscape:window-height="972"
+     inkscape:window-x="1280"
+     inkscape:window-y="0"
+     width="205.7px"
+     height="195.1px" />
+  <metadata
+     id="metadata1917">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-422.7446,-436.519)">
+    <g
+       transform="matrix(1.74931,0,0,1.74931,184.7276,168.542)"
+       id="g1975">
+      <rect
+         ry="0"
+         rx="0"
+         y="156.55508"
+         x="140"
+         height="88.664246"
+         width="111.42859"
+         id="rect1977"
+         style="opacity:1;fill:url(#linearGradient1981);fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:3.29999995;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <path
+         sodipodi:nodetypes="ccccccccc"
+         id="path1979"
+         d="M 192.7346,245.21932 L 191.40012,258.79075 L 159.16317,258.79075 L 164.16317,263.07647 L 227.7346,263.07647 L 219.98597,258.6028 L 196.40012,258.6028 L 198.52981,244.85366 L 192.7346,245.21932 z "
+         style="fill:black;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    </g>
+    <g
+       id="g2056"
+       transform="translate(227.5714,-199.5714)">
+      <rect
+         y="667.27252"
+         x="243.60435"
+         height="102.32214"
+         width="109.4402"
+         id="rect2993"
+         style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1.76163042;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <rect
+         y="667.27252"
+         x="243.82677"
+         height="7.7853804"
+         width="108.99533"
+         id="rect2999"
+         style="opacity:1;fill:url(#linearGradient2060);fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1.76163042;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    </g>
+    <path
+       id="path3099"
+       style="fill:red;fill-opacity:1;fill-rule:evenodd;stroke:red;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
+       d="M 527.7278,490.20483 L 518.79619,495.55345 L 527.71248,490.22015 L 518.85148,484.54087 L 527.74292,490.22432 L 472.93941,490.22432 L 481.83085,484.54087 L 472.96985,490.22015 L 481.88614,495.55345 L 472.95453,490.20483"
+       sodipodi:nodetypes="cccccccccc" />
+    <text
+       sodipodi:linespacing="125%"
+       id="text3105"
+       y="487.25659"
+       x="481.51767"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:red;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       xml:space="preserve"><tspan
+         y="487.25659"
+         x="481.51767"
+         id="tspan3107"
+         sodipodi:role="line">x</tspan></text>
+    <text
+       transform="matrix(0,-1,1,0,0,0)"
+       sodipodi:linespacing="125%"
+       id="text3109"
+       y="541.07733"
+       x="-555.61816"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:red;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       xml:space="preserve"><tspan
+         y="541.07733"
+         x="-555.61816"
+         id="tspan3111"
+         sodipodi:role="line">y</tspan></text>
+    <path
+       sodipodi:nodetypes="cccccccccc"
+       d="M 546.8512,569.18028 L 541.50257,560.24867 L 546.83588,569.16496 L 552.51516,560.30396 L 546.83171,569.1954 L 546.83171,500.4276 L 552.51516,509.31904 L 546.83588,500.45804 L 541.50257,509.37433 L 546.8512,500.44272"
+       style="fill:red;fill-opacity:1;fill-rule:evenodd;stroke:red;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
+       id="path3101" />
+    <g
+       id="g3310"
+       transform="matrix(0.716716,-0.697365,0.697365,0.716716,-191.9167,514.2255)">
+      <path
+         transform="translate(206.5126,430.519)"
+         id="path3306"
+         d="M 320,62.850006 L 316.65192,68.649044 L 323.0625,68.649044 L 320,62.850006 z "
+         style="fill:black;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+      <path
+         sodipodi:nodetypes="cc"
+         transform="translate(206.5126,430.519)"
+         id="path3308"
+         d="M 319.875,68.975006 L 319.875,74.162506"
+         style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:black;stroke-width:3.29999995;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    </g>
+    <path
+       transform="translate(210.5126,433.519)"
+       d="M 319.125 66.225006 A 1.25 1.25 0 1 1  316.625,66.225006 A 1.25 1.25 0 1 1  319.125 66.225006 z"
+       sodipodi:ry="1.25"
+       sodipodi:rx="1.25"
+       sodipodi:cy="66.225006"
+       sodipodi:cx="317.875"
+       id="path3316"
+       style="opacity:1;fill:red;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.29999995;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       sodipodi:type="arc" />
+    <path
+       sodipodi:type="arc"
+       style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:red;stroke-width:0.60000002;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="path3314"
+       sodipodi:cx="317.875"
+       sodipodi:cy="66.225006"
+       sodipodi:rx="2.625"
+       sodipodi:ry="2.625"
+       d="M 320.5 66.225006 A 2.625 2.625 0 1 1  315.25,66.225006 A 2.625 2.625 0 1 1  320.5 66.225006 z"
+       transform="translate(210.5126,433.519)" />
+  </g>
+</svg>
diff --git a/doc/programming_guide/img/screens.png b/doc/programming_guide/img/screens.png
new file mode 100644
index 0000000..8b658f0
Binary files /dev/null and b/doc/programming_guide/img/screens.png differ
diff --git a/doc/programming_guide/img/screens.svg b/doc/programming_guide/img/screens.svg
new file mode 100644
index 0000000..c236925
--- /dev/null
+++ b/doc/programming_guide/img/screens.svg
@@ -0,0 +1,258 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="496.88"
+   height="237.97"
+   id="svg1952"
+   sodipodi:version="0.32"
+   inkscape:version="0.44.1"
+   sodipodi:docbase="/home/alex/projects/pyglet/doc"
+   sodipodi:docname="screens.svg"
+   version="1.0">
+  <defs
+     id="defs1954">
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient2786">
+      <stop
+         style="stop-color:#00d3ff;stop-opacity:1;"
+         offset="0"
+         id="stop2788" />
+      <stop
+         style="stop-color:#00d3ff;stop-opacity:0;"
+         offset="1"
+         id="stop2790" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2786"
+       id="linearGradient1981"
+       gradientUnits="userSpaceOnUse"
+       x1="193.03572"
+       y1="160.93361"
+       x2="193.03572"
+       y2="232.54076" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2786"
+       id="linearGradient3097"
+       gradientUnits="userSpaceOnUse"
+       x1="193.03572"
+       y1="160.93361"
+       x2="193.03572"
+       y2="232.54076" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.7"
+     inkscape:cx="350.32817"
+     inkscape:cy="53.993003"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     inkscape:window-width="1230"
+     inkscape:window-height="972"
+     inkscape:window-x="1280"
+     inkscape:window-y="0"
+     width="496.88px"
+     height="237.97px" />
+  <metadata
+     id="metadata1957">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-84.11113,-98.73948)">
+    <g
+       id="g3091"
+       transform="matrix(1.74931,0,0,1.74931,-117.9528,-154.3791)">
+      <rect
+         style="opacity:1;fill:url(#linearGradient3097);fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:3.29999995;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="rect3093"
+         width="111.42859"
+         height="88.664246"
+         x="140"
+         y="156.55508"
+         rx="0"
+         ry="0" />
+      <path
+         style="fill:black;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         d="M 192.7346,245.21932 L 191.40012,258.79075 L 159.16317,258.79075 L 164.16317,263.07647 L 227.7346,263.07647 L 219.98597,258.6028 L 196.40012,258.6028 L 198.52981,244.85366 L 192.7346,245.21932 z "
+         id="path3095"
+         sodipodi:nodetypes="ccccccccc" />
+    </g>
+    <g
+       transform="matrix(1.74931,0,0,1.74931,132.565,-154.3791)"
+       id="g1975">
+      <rect
+         ry="0"
+         rx="0"
+         y="156.55508"
+         x="140"
+         height="88.664246"
+         width="111.42859"
+         id="rect1977"
+         style="opacity:1;fill:url(#linearGradient1981);fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:3.29999995;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <path
+         sodipodi:nodetypes="ccccccccc"
+         id="path1979"
+         d="M 192.7346,245.21932 L 191.40012,258.79075 L 159.16317,258.79075 L 164.16317,263.07647 L 227.7346,263.07647 L 219.98597,258.6028 L 196.40012,258.6028 L 198.52981,244.85366 L 192.7346,245.21932 z "
+         style="fill:black;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    </g>
+    <path
+       id="path3099"
+       style="fill:red;fill-opacity:1;fill-rule:evenodd;stroke:red;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
+       d="M 318.85089,320.51586 L 309.91928,325.86448 L 318.83557,320.53118 L 309.97457,314.8519 L 318.86601,320.53535 L 130.49107,320.53535 L 139.38251,314.8519 L 130.52151,320.53118 L 139.4378,325.86448 L 130.50619,320.51586"
+       sodipodi:nodetypes="cccccccccc" />
+    <path
+       sodipodi:nodetypes="cccccccccc"
+       d="M 569.36872,320.51586 L 560.43711,325.86448 L 569.3534,320.53118 L 560.4924,314.8519 L 569.38384,320.53535 L 381.0089,320.53535 L 389.90034,314.8519 L 381.03934,320.53118 L 389.95563,325.86448 L 381.02402,320.51586"
+       style="fill:red;fill-opacity:1;fill-rule:evenodd;stroke:red;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
+       id="path3071" />
+    <path
+       sodipodi:nodetypes="cccccccccc"
+       d="M 102.65286,270.8663 L 97.304239,261.93469 L 102.63754,270.85098 L 108.31682,261.98998 L 102.63337,270.88142 L 102.63337,122.50648 L 108.31682,131.39792 L 102.63754,122.53692 L 97.304239,131.45321 L 102.65286,122.5216"
+       style="fill:red;fill-opacity:1;fill-rule:evenodd;stroke:red;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
+       id="path3101" />
+    <path
+       id="path3058"
+       style="fill:red;fill-opacity:1;fill-rule:evenodd;stroke:red;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
+       d="M 353.17069,270.8663 L 347.82207,261.93469 L 353.15537,270.85098 L 358.83465,261.98998 L 353.1512,270.88142 L 353.1512,122.50648 L 358.83465,131.39792 L 353.15537,122.53692 L 347.82207,131.45321 L 353.17069,122.5216"
+       sodipodi:nodetypes="cccccccccc" />
+    <path
+       id="path3103"
+       d="M 131.56494,112.06861 L 131.56494,136.81735 L 131.56494,123.68537 L 142.67661,123.68537 L 120.95833,123.68537 L 132.07001,123.68537 L 131.56494,112.06861 z "
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:red;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:red;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 382.08277,112.06861 L 382.08277,136.81735 L 382.08277,123.68537 L 393.19444,123.68537 L 371.47616,123.68537 L 382.58784,123.68537 L 382.08277,112.06861 z "
+       id="path3077" />
+    <text
+       sodipodi:linespacing="125%"
+       id="text3105"
+       y="332.28186"
+       x="146.21216"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:red;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       xml:space="preserve"><tspan
+         y="332.28186"
+         x="146.21216"
+         id="tspan3107"
+         sodipodi:role="line">width = 1280</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:red;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       x="396.72998"
+       y="332.28186"
+       id="text3079"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3081"
+         x="396.72998"
+         y="332.28186">width = 1280</tspan></text>
+    <text
+       transform="matrix(0,-1,1,0,0,0)"
+       sodipodi:linespacing="125%"
+       id="text3109"
+       y="97.021843"
+       x="-258.30414"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:red;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       xml:space="preserve"><tspan
+         y="97.021843"
+         x="-258.30414"
+         id="tspan3111"
+         sodipodi:role="line">height = 1024</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:red;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       x="-258.30414"
+       y="347.53967"
+       id="text3083"
+       sodipodi:linespacing="125%"
+       transform="matrix(0,-1,1,0,0,0)"><tspan
+         sodipodi:role="line"
+         id="tspan3085"
+         x="-258.30414"
+         y="347.53967">height = 1024</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text3113"
+       y="113.07877"
+       x="138.63602"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:red;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       xml:space="preserve"><tspan
+         y="113.07877"
+         x="138.63602"
+         id="tspan3115"
+         sodipodi:role="line">x = 0, y = 0</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:red;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       x="389.15384"
+       y="113.07877"
+       id="text3087"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3089"
+         x="389.15384"
+         y="113.07877">x = 1280, y = 0</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:48px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       x="457.84421"
+       y="215.60925"
+       id="text3145"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3147"
+         x="457.84421"
+         y="215.60925">1</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text3139"
+       y="215.60925"
+       x="212.37715"
+       style="font-size:48px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       xml:space="preserve"><tspan
+         y="215.60925"
+         x="212.37715"
+         id="tspan3141"
+         sodipodi:role="line">2</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:48px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:black;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       x="201.02037"
+       y="209.89497"
+       id="text3135"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3137"
+         x="201.02037"
+         y="209.89497" /></text>
+  </g>
+</svg>
diff --git a/doc/programming_guide/img/text_classes.png b/doc/programming_guide/img/text_classes.png
new file mode 100644
index 0000000..56ae799
Binary files /dev/null and b/doc/programming_guide/img/text_classes.png differ
diff --git a/doc/programming_guide/img/text_classes.svg b/doc/programming_guide/img/text_classes.svg
new file mode 100644
index 0000000..152357d
--- /dev/null
+++ b/doc/programming_guide/img/text_classes.svg
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="534.40002"
+   height="231.73"
+   id="svg2427"
+   sodipodi:version="0.32"
+   inkscape:version="0.46"
+   sodipodi:docname="text_classes.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape"
+   version="1.0">
+  <defs
+     id="defs2429">
+    <marker
+       inkscape:stockid="Arrow2Lstart"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow2Lstart"
+       style="overflow:visible">
+      <path
+         id="path4308"
+         style="font-size:12px;fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round"
+         d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.97309,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z"
+         transform="matrix(1.1,0,0,1.1,1.1,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow2Mstart"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow2Mstart"
+       style="overflow:visible">
+      <path
+         id="path4314"
+         style="font-size:12px;fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round"
+         d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.97309,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z"
+         transform="scale(0.6,0.6)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow2Mend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow2Mend"
+       style="overflow:visible">
+      <path
+         id="path4317"
+         style="font-size:12px;fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round"
+         d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.97309,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z"
+         transform="scale(-0.6,-0.6)" />
+    </marker>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 526.18109 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="744.09448 : 526.18109 : 1"
+       inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+       id="perspective2435" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="305.16108"
+     inkscape:cy="83.8478"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1032"
+     inkscape:window-height="888"
+     inkscape:window-x="1523"
+     inkscape:window-y="49" />
+  <metadata
+     id="metadata2432">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-180.25727,-41.011303)">
+    <rect
+       style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect2465"
+       width="151.24506"
+       height="56.731632"
+       x="475.9534"
+       y="44.511303" />
+    <rect
+       style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect2477"
+       width="151.24506"
+       height="56.731632"
+       x="183.9534"
+       y="44.511303" />
+    <rect
+       style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect2485"
+       width="151.24506"
+       height="56.731632"
+       x="183.9534"
+       y="129.51131" />
+    <rect
+       y="213.51131"
+       x="183.9534"
+       height="56.731632"
+       width="151.24506"
+       id="rect2804"
+       style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+       x="551.4574"
+       y="58.691605"
+       id="text2467"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         x="551.4574"
+         y="58.691605"
+         id="tspan2469"
+         style="font-style:italic;font-family:Arial">AbstractDocument</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+       x="259.4574"
+       y="58.691605"
+       id="text2479"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         x="259.4574"
+         y="58.691605"
+         id="tspan2481"
+         style="font-style:normal;font-family:Arial">TextLayout</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+       x="259.4574"
+       y="143.6916"
+       id="text2487"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         x="259.4574"
+         y="143.6916"
+         id="tspan2489"
+         style="font-style:normal;font-family:Arial">ScrollableTextLayout</tspan></text>
+    <text
+       sodipodi:linespacing="125%"
+       id="text2806"
+       y="227.6916"
+       x="259.4574"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+       xml:space="preserve"><tspan
+         style="font-style:normal;font-family:Arial"
+         id="tspan2808"
+         y="227.6916"
+         x="259.4574"
+         sodipodi:role="line">IncrementalTextLayout</tspan></text>
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 475.75727,63.742391 L 627.21886,63.742391"
+       id="path2471"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 183.75727,63.742391 L 335.21886,63.742391"
+       id="path2483"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 183.75727,148.74239 L 335.21886,148.74239"
+       id="path2491"
+       sodipodi:nodetypes="cc" />
+    <path
+       sodipodi:nodetypes="cc"
+       id="path2810"
+       d="M 183.75727,232.74239 L 335.21886,232.74239"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="cccc"
+       id="path2473"
+       d="M 549.96757,101.78996 L 543.53934,111.39305 L 556.15476,111.39305 L 549.96757,101.78996 z"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:bevel;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="cccc"
+       id="path2493"
+       d="M 257.96757,101.78996 L 251.53934,111.39305 L 264.15476,111.39305 L 257.96757,101.78996 z"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:bevel;stroke-opacity:1" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:bevel;stroke-opacity:1"
+       d="M 257.96757,185.78996 L 251.53934,195.39305 L 264.15476,195.39305 L 257.96757,185.78996 z"
+       id="path2770"
+       sodipodi:nodetypes="cccc" />
+    <path
+       sodipodi:nodetypes="cc"
+       id="path2475"
+       d="M 549.88197,111.39448 L 549.88197,128.39752"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 257.88197,111.39448 L 257.88197,128.39752"
+       id="path2497"
+       sodipodi:nodetypes="cc" />
+    <path
+       sodipodi:nodetypes="cc"
+       id="path2495"
+       d="M 475.67086,55.179491 L 334.95661,54.858415"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;marker-start:url(#Arrow2Lstart);marker-end:none;stroke-opacity:1" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 257.88197,195.39448 L 257.88197,212.39752"
+       id="path2772"
+       sodipodi:nodetypes="cc" />
+    <path
+       id="path3009"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 469.19052,151.07021 L 469.19052,128.74878 L 637.21429,128.74878 L 637.38092,128.74878 L 637.38092,151.07021"
+       sodipodi:nodetypes="ccccc" />
+    <g
+       transform="translate(367.03571,8.642861)"
+       id="g2455">
+      <rect
+         y="142.21065"
+         x="25.358196"
+         height="56.731632"
+         width="151.24506"
+         id="rect2457"
+         style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <text
+         sodipodi:linespacing="125%"
+         id="text2459"
+         y="156.39101"
+         x="100.86221"
+         style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+         xml:space="preserve"><tspan
+           style="font-family:Arial"
+           id="tspan2461"
+           y="156.39101"
+           x="100.86221"
+           sodipodi:role="line">UnformattedDocument</tspan></text>
+      <path
+         sodipodi:nodetypes="cc"
+         id="path2463"
+         d="M 25.162063,161.44174 L 176.62366,161.44174"
+         style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    </g>
+    <g
+       id="g2956"
+       transform="translate(533.53571,8.642861)">
+      <rect
+         style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+         id="rect2816"
+         width="151.24506"
+         height="56.731632"
+         x="25.358196"
+         y="142.21065" />
+      <text
+         xml:space="preserve"
+         style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Helvetica"
+         x="100.86221"
+         y="156.39101"
+         id="text2818"
+         sodipodi:linespacing="125%"><tspan
+           sodipodi:role="line"
+           x="100.86221"
+           y="156.39101"
+           id="tspan2820"
+           style="font-family:Arial">FormattedDocument</tspan></text>
+      <path
+         style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         d="M 25.162063,161.44174 L 176.62366,161.44174"
+         id="path2822"
+         sodipodi:nodetypes="cc" />
+    </g>
+  </g>
+</svg>
diff --git a/doc/programming_guide/img/window_location.png b/doc/programming_guide/img/window_location.png
new file mode 100644
index 0000000..1971b75
Binary files /dev/null and b/doc/programming_guide/img/window_location.png differ
diff --git a/doc/programming_guide/img/window_location.svg b/doc/programming_guide/img/window_location.svg
new file mode 100644
index 0000000..26c081d
--- /dev/null
+++ b/doc/programming_guide/img/window_location.svg
@@ -0,0 +1,234 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="420.92999"
+   height="199.10001"
+   id="svg1912"
+   sodipodi:version="0.32"
+   inkscape:version="0.44.1"
+   sodipodi:docbase="/home/alex/projects/pyglet/doc"
+   sodipodi:docname="window_location.svg"
+   version="1.0">
+  <defs
+     id="defs1914">
+    <linearGradient
+       id="linearGradient2794">
+      <stop
+         style="stop-color:#0038ff;stop-opacity:1;"
+         offset="0"
+         id="stop2796" />
+      <stop
+         style="stop-color:black;stop-opacity:1;"
+         offset="1"
+         id="stop2798" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2786"
+       id="linearGradient3097"
+       gradientUnits="userSpaceOnUse"
+       x1="193.03572"
+       y1="160.93361"
+       x2="193.03572"
+       y2="232.54076" />
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient2786">
+      <stop
+         style="stop-color:#00d3ff;stop-opacity:1;"
+         offset="0"
+         id="stop2788" />
+      <stop
+         style="stop-color:#00d3ff;stop-opacity:0;"
+         offset="1"
+         id="stop2790" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2786"
+       id="linearGradient1981"
+       gradientUnits="userSpaceOnUse"
+       x1="193.03572"
+       y1="160.93361"
+       x2="193.03572"
+       y2="232.54076" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2794"
+       id="linearGradient2060"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.76163,0,0,1.76163,-60.69281,353.5698)"
+       x1="177.85715"
+       y1="180.93361"
+       x2="232.18196"
+       y2="180.93361" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="201.28937"
+     inkscape:cy="121.06366"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     inkscape:window-width="1230"
+     inkscape:window-height="972"
+     inkscape:window-x="1280"
+     inkscape:window-y="0"
+     width="420.93px"
+     height="199.1px" />
+  <metadata
+     id="metadata1917">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-206.5126,-430.519)">
+    <g
+       id="g3091"
+       transform="matrix(1.74931,0,0,1.74931,-30.50443,165.542)">
+      <rect
+         style="opacity:1;fill:url(#linearGradient3097);fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:3.29999995;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="rect3093"
+         width="111.42859"
+         height="88.664246"
+         x="140"
+         y="156.55508"
+         rx="0"
+         ry="0" />
+      <path
+         style="fill:black;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         d="M 192.7346,245.21932 L 191.40012,258.79075 L 159.16317,258.79075 L 164.16317,263.07647 L 227.7346,263.07647 L 219.98597,258.6028 L 196.40012,258.6028 L 198.52981,244.85366 L 192.7346,245.21932 z "
+         id="path3095"
+         sodipodi:nodetypes="ccccccccc" />
+    </g>
+    <g
+       transform="matrix(1.74931,0,0,1.74931,180.7276,165.542)"
+       id="g1975">
+      <rect
+         ry="0"
+         rx="0"
+         y="156.55508"
+         x="140"
+         height="88.664246"
+         width="111.42859"
+         id="rect1977"
+         style="opacity:1;fill:url(#linearGradient1981);fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:3.29999995;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <path
+         sodipodi:nodetypes="ccccccccc"
+         id="path1979"
+         d="M 192.7346,245.21932 L 191.40012,258.79075 L 159.16317,258.79075 L 164.16317,263.07647 L 227.7346,263.07647 L 219.98597,258.6028 L 196.40012,258.6028 L 198.52981,244.85366 L 192.7346,245.21932 z "
+         style="fill:black;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    </g>
+    <path
+       id="path3099"
+       style="fill:red;fill-opacity:1;fill-rule:evenodd;stroke:red;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
+       d="M 465.7278,467.57983 L 456.79619,472.92845 L 465.71248,467.59515 L 456.85148,461.91587 L 465.74292,467.59932 L 217.93941,467.59932 L 226.83085,461.91587 L 217.96985,467.59515 L 226.88614,472.92845 L 217.95453,467.57983"
+       sodipodi:nodetypes="cccccccccc" />
+    <path
+       sodipodi:nodetypes="cccccccccc"
+       d="M 574.67421,576.86555 L 565.7426,582.21417 L 574.65889,576.88087 L 565.79789,571.20159 L 574.68933,576.88504 L 468.45724,576.88504 L 477.34868,571.20159 L 468.48768,576.88087 L 477.40397,582.21417 L 468.47236,576.86555"
+       style="fill:red;fill-opacity:1;fill-rule:evenodd;stroke:red;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
+       id="path3071" />
+    <path
+       id="path3058"
+       style="fill:red;fill-opacity:1;fill-rule:evenodd;stroke:red;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
+       d="M 454.19046,563.64456 L 448.84184,554.71295 L 454.17514,563.62924 L 459.85442,554.76824 L 454.17097,563.65968 L 454.17097,473.85617 L 459.85442,482.74761 L 454.17514,473.88661 L 448.84184,482.8029 L 454.19046,473.87129"
+       sodipodi:nodetypes="cccccccccc" />
+    <text
+       sodipodi:linespacing="125%"
+       id="text3105"
+       y="464.63159"
+       x="437.51767"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:red;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       xml:space="preserve"><tspan
+         y="464.63159"
+         x="437.51767"
+         id="tspan3107"
+         sodipodi:role="line">x</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:red;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       x="479.89261"
+       y="587.9173"
+       id="text3079"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3081"
+         x="479.89261"
+         y="587.9173">width</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:red;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       x="-550.36816"
+       y="448.63089"
+       id="text3083"
+       sodipodi:linespacing="125%"
+       transform="matrix(0,-1,1,0,0,0)"><tspan
+         sodipodi:role="line"
+         id="tspan3085"
+         x="-550.36816"
+         y="448.63089">height</tspan></text>
+    <g
+       id="g2056"
+       transform="translate(223.5714,-202.5714)">
+      <rect
+         y="667.27252"
+         x="243.60435"
+         height="102.32214"
+         width="109.4402"
+         id="rect2993"
+         style="opacity:1;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1.76163042;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <rect
+         y="667.27252"
+         x="243.82677"
+         height="7.7853804"
+         width="108.99533"
+         id="rect2999"
+         style="opacity:1;fill:url(#linearGradient2060);fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1.76163042;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    </g>
+    <path
+       sodipodi:nodetypes="cccccccccc"
+       d="M 475.6012,472.68028 L 470.25257,463.74867 L 475.58588,472.66496 L 481.26516,463.80396 L 475.58171,472.6954 L 475.58171,442.1776 L 481.26516,451.06904 L 475.58588,442.20804 L 470.25257,451.12433 L 475.6012,442.19272"
+       style="fill:red;fill-opacity:1;fill-rule:evenodd;stroke:red;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
+       id="path3101" />
+    <text
+       transform="matrix(0,-1,1,0,0,0)"
+       sodipodi:linespacing="125%"
+       id="text3109"
+       y="469.82733"
+       x="-459.11813"
+       style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:red;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial"
+       xml:space="preserve"><tspan
+         y="469.82733"
+         x="-459.11813"
+         id="tspan3111"
+         sodipodi:role="line">y</tspan></text>
+  </g>
+</svg>
diff --git a/doc/programming_guide/img/window_osx_default.png b/doc/programming_guide/img/window_osx_default.png
new file mode 100644
index 0000000..48ee012
Binary files /dev/null and b/doc/programming_guide/img/window_osx_default.png differ
diff --git a/doc/programming_guide/img/window_osx_dialog.png b/doc/programming_guide/img/window_osx_dialog.png
new file mode 100644
index 0000000..da2af82
Binary files /dev/null and b/doc/programming_guide/img/window_osx_dialog.png differ
diff --git a/doc/programming_guide/img/window_osx_tool.png b/doc/programming_guide/img/window_osx_tool.png
new file mode 100644
index 0000000..3a7a488
Binary files /dev/null and b/doc/programming_guide/img/window_osx_tool.png differ
diff --git a/doc/programming_guide/img/window_xp_default.png b/doc/programming_guide/img/window_xp_default.png
new file mode 100644
index 0000000..bd10024
Binary files /dev/null and b/doc/programming_guide/img/window_xp_default.png differ
diff --git a/doc/programming_guide/img/window_xp_dialog.png b/doc/programming_guide/img/window_xp_dialog.png
new file mode 100644
index 0000000..151ac2a
Binary files /dev/null and b/doc/programming_guide/img/window_xp_dialog.png differ
diff --git a/doc/programming_guide/img/window_xp_overlay.png b/doc/programming_guide/img/window_xp_overlay.png
new file mode 100644
index 0000000..e8e2000
Binary files /dev/null and b/doc/programming_guide/img/window_xp_overlay.png differ
diff --git a/doc/programming_guide/img/window_xp_tool.png b/doc/programming_guide/img/window_xp_tool.png
new file mode 100644
index 0000000..d067571
Binary files /dev/null and b/doc/programming_guide/img/window_xp_tool.png differ
diff --git a/doc/programming_guide/img/window_xp_transparent.png b/doc/programming_guide/img/window_xp_transparent.png
new file mode 100644
index 0000000..6088399
Binary files /dev/null and b/doc/programming_guide/img/window_xp_transparent.png differ
diff --git a/doc/programming_guide/input.rst b/doc/programming_guide/input.rst
new file mode 100644
index 0000000..aadf76f
--- /dev/null
+++ b/doc/programming_guide/input.rst
@@ -0,0 +1,143 @@
+Working with other input devices
+================================
+
+Pyglet's :py:mod:`~pyglet.input` module allows you to accept input
+from any USB human interface device (HID).  High level interfaces
+are provided for working with joysticks and with the Apple Remote.
+
+Using joysticks
+---------------
+
+Before using a joystick, you must find it and open it.  To get a list
+of all joystick devices currently connected to your computer, call
+:py:func:`pyglet.input.get_joysticks`::
+
+    joysticks = pyglet.input.get_joysticks()
+
+Then choose a joystick from the list and call `Joystick.open` to open
+the device::
+
+    if joysticks:
+        joystick = joysticks[0]
+    joystick.open()
+
+The current position of the joystick is recorded in its 'x' and 'y'
+attributes, both of which are normalized to values within the range
+of -1 to 1.  For the x-axis, `x` = -1 means the joystick is pushed
+all the way to the left and `x` = 1 means the joystick is pushed to the right.
+For the y-axis, a value of `y` = -1 means that the joystick is pushed up
+and a value of `y` = 1 means that the joystick is pushed down.
+
+If your joystick has two analog controllers, the position of the
+second controller is typically given by `z` and `rz`, where `z` is the
+horizontal axis position and `rz` is the vertical axis position.
+
+The state of the joystick buttons is contained in the `buttons`
+attribute as a list of boolean values.  A True value indicates that
+the corresponding button is being pressed.  While buttons may be
+labeled A, B, X, or Y on the physical joystick, they are simply
+referred to by their index when accessing the `buttons` list. There
+is no easy way to know which button index corresponds to which
+physical button on the device without testing the particular joystick,
+so it is a good idea to let users change button assignments.
+
+Each open joystick dispatches events when the joystick changes state.
+For buttons, there is the :py:meth:`~pyglet.input.Joystick.on_joybutton_press`
+event which is sent whenever any of the joystick's buttons are pressed::
+
+    def on_joybutton_press(joystick, button):
+        pass
+
+and the :py:meth:`~pyglet.input.Joystick.on_joybutton_release` event which is
+sent whenever any of the joystick's buttons are released::
+
+    def on_joybutton_release(joystick, button):
+        pass
+
+The :py:class:`~pyglet.input.Joystick` parameter is the
+:py:class:`~pyglet.input.Joystick` instance whose buttons changed state
+(useful if you have multiple joysticks connected).
+The `button` parameter signifies which button changed and is simply an
+integer value, the index of the corresponding button in the `buttons`
+list.
+
+For most games, it is probably best to examine the current position of
+the joystick directly by using the `x` and `y` attributes.  However if
+you want to receive notifications whenever these values change you
+should handle the :py:meth:`~pyglet.input.Joystick.on_joyaxis_motion` event::
+
+    def on_joyaxis_motion(joystick, axis, value):
+        pass
+
+The :py:class:`~pyglet.input.Joystick` parameter again tells you which
+joystick device changed.  The `axis` parameter is string such as
+"x", "y", or "rx" telling you which axis changed value.  And `value`
+gives the current normalized value of the axis, ranging between -1 and 1.
+
+If the joystick has a hat switch, you may examine its current value by
+looking at the `hat_x` and `hat_y` attributes.  For both, the values
+are either -1, 0, or 1.  Note that `hat_y` will output 1 in the up
+position and -1 in the down position, which is the opposite of the
+y-axis control.
+
+To be notified when the hat switch changes value, handle the
+:py:meth:`~pyglet.input.Joystick.on_joyhat_motion` event::
+
+    def on_joyhat_motion(joystick, hat_x, hat_y):
+        pass
+
+The `hat_x` and `hat_y` parameters give the same values as the
+joystick's `hat_x` and `hat_y` attributes.
+
+A good way to use the joystick event handlers might be to define them
+within a controller class and then call::
+
+    joystick.push_handlers(my_controller)
+
+Please note that you need a running application event loop for the joystick
+button an axis values to be properly updated. See the
+:ref:`programming-guide-eventloop` section for more details on how to start
+an event loop.
+
+
+Using the Apple Remote
+----------------------
+
+The Apple Remote is a small infrared remote originally distributed
+with the iMac.  The remote has six buttons, which are accessed with
+the names `left`, `right`, `up`, `down`, `menu`, and `select`.
+Additionally when certain buttons are held down, they act as virtual
+buttons.  These are named `left_hold`, `right_hold`, `menu_hold`, and
+`select_hold`.
+
+To use the remote, first call :py:func:`~pyglet.input.get_apple_remote`::
+
+    remote = pyglet.input.get_apple_remote()
+
+Then open it::
+
+    if remote:
+        remote.open(window, exclusive=True)
+
+The remote is opened in exclusive mode so that while we are using the
+remote in our program, pressing the buttons does not activate Front
+Row, or change the volume, etc. on the computer.
+
+The following event handlers tell you when a button on the remote has
+been either pressed or released::
+
+    def on_button_press(button):
+        pass
+
+    def on_button_release(button):
+        pass
+
+The `button` parameter indicates which button changed and is a string
+equal to one of the ten button names defined above: "up", "down",
+"left", "left_hold", "right",  "right_hold", "select", "select_hold",
+"menu", or "menu_hold".
+
+To use the remote, you may define code for the event handlers in
+some controller class and then call::
+
+    remote.push_handlers(my_controller)
diff --git a/doc/programming_guide/installation.rst b/doc/programming_guide/installation.rst
new file mode 100644
index 0000000..4776716
--- /dev/null
+++ b/doc/programming_guide/installation.rst
@@ -0,0 +1,49 @@
+Installation
+============
+
+.. note:: These instructions apply to pyglet |version|.
+
+pyglet is a pure python library, so no special steps are required for
+installation. You can install it in a variety of ways, or simply copy the
+`pyglet` folder directly into your project. If you're unsure what to do,
+the recommended method is to install it into your local ``site-packages``
+directory. pyglet is available `on PyPI <https://pypi.python.org/pypi/pyglet>`_.
+and can be installed like any other Python library via **pip**:
+
+.. code-block:: sh
+
+    pip install pyglet --user
+
+You can also clone the repository using **git** and install from source:
+
+.. code-block:: sh
+
+    git clone https://github.com/pyglet/pyglet.git
+    cd pyglet
+    python setup.py install --user
+
+
+To play compressed audio and video files (anything except for WAV), you will need
+`FFmpeg <https://www.ffmpeg.org/download.html>`_.
+
+
+Running the examples
+--------------------
+
+The source code archives include examples. Archives are
+`available on Github <https://github.com/pyglet/pyglet/releases/>`_:
+
+.. code-block:: sh
+
+    unzip pyglet-x.x.x.zip
+    cd pyglet-x.x.x
+    python examples/hello_world.py
+
+
+As mentioned above, you can also clone the repository using Git:
+
+.. code-block:: sh
+
+    git clone https://github.com/pyglet/pyglet.git
+    cd pyglet
+    python examples/hello_world.py
diff --git a/doc/programming_guide/keyboard.rst b/doc/programming_guide/keyboard.rst
new file mode 100644
index 0000000..771e115
--- /dev/null
+++ b/doc/programming_guide/keyboard.rst
@@ -0,0 +1,347 @@
+.. _guide_working-with-the-keyboard:
+
+Working with the keyboard
+=========================
+
+pyglet has support for low-level keyboard input suitable for games as well as
+locale- and device-independent Unicode text entry.
+
+Keyboard input requires a window which has focus.  The operating system
+usually decides which application window has keyboard focus.  Typically this
+window appears above all others and may be decorated differently, though this
+is platform-specific (for example, Unix window managers sometimes couple
+keyboard focus with the mouse pointer).
+
+You can request keyboard focus for a window with the
+:py:meth:`~pyglet.window.Window.activate` method, but you should not rely
+on this -- it may simply provide a visual cue to the user indicating that
+the window requires user input, without actually getting focus.
+
+Windows created with the
+:py:attr:`~pyglet.window.Window.WINDOW_STYLE_BORDERLESS` or
+:py:attr:`~pyglet.window.Window.WINDOW_STYLE_TOOL`
+style cannot receive keyboard focus.
+
+It is not possible to use pyglet's keyboard or text events without a window;
+consider using Python built-in functions such as ``input`` instead.
+
+Keyboard events
+---------------
+
+The :py:meth:`~pyglet.window.Window.on_key_press` and
+:py:meth:`~pyglet.window.Window.on_key_release` events are fired when
+any key on the keyboard is pressed or released, respectively.  These events
+are not affected by "key repeat" -- once a key is pressed there are no more
+events for that key until it is released.
+
+Both events are parameterised by the same arguments::
+
+    def on_key_press(symbol, modifiers):
+        pass
+
+    def on_key_release(symbol, modifiers):
+        pass
+
+Defined key symbols
+^^^^^^^^^^^^^^^^^^^
+
+The `symbol` argument is an integer that represents a "virtual" key code.
+It does *not* correspond to any particular numbering scheme; in particular
+the symbol is *not* an ASCII character code.
+
+pyglet has key symbols that are hardware and platform independent
+for many types of keyboard.  These are defined in
+:py:mod:`pyglet.window.key` as constants.  For example, the Latin-1
+alphabet is simply the letter itself::
+
+    key.A
+    key.B
+    key.C
+    ...
+
+The numeric keys have an underscore to make them valid identifiers::
+
+    key._1
+    key._2
+    key._3
+    ...
+
+Various control and directional keys are identified by name::
+
+    key.ENTER or key.RETURN
+    key.SPACE
+    key.BACKSPACE
+    key.DELETE
+    key.MINUS
+    key.EQUAL
+    key.BACKSLASH
+
+    key.LEFT
+    key.RIGHT
+    key.UP
+    key.DOWN
+    key.HOME
+    key.END
+    key.PAGEUP
+    key.PAGEDOWN
+
+    key.F1
+    key.F2
+    ...
+
+Keys on the number pad have separate symbols::
+
+    key.NUM_1
+    key.NUM_2
+    ...
+    key.NUM_EQUAL
+    key.NUM_DIVIDE
+    key.NUM_MULTIPLY
+    key.NUM_SUBTRACT
+    key.NUM_ADD
+    key.NUM_DECIMAL
+    key.NUM_ENTER
+
+Some modifier keys have separate symbols for their left and right sides
+(however they cannot all be distinguished on all platforms, including Mac OS
+X)::
+
+    key.LCTRL
+    key.RCTRL
+    key.LSHIFT
+    key.RSHIFT
+    ...
+
+Key symbols are independent of any modifiers being held down.  For example,
+lower-case and upper-case letters both generate the `A` symbol.  This is also
+true of the number keypad.
+
+Modifiers
+^^^^^^^^^
+
+The modifiers that are held down when the event is generated are combined in a
+bitwise fashion and provided in the ``modifiers`` parameter.  The modifier
+constants defined in :py:mod:`pyglet.window.key` are::
+
+    MOD_SHIFT
+    MOD_CTRL
+    MOD_ALT         Not available on Mac OS X
+    MOD_WINDOWS     Available on Windows only
+    MOD_COMMAND     Available on Mac OS X only
+    MOD_OPTION      Available on Mac OS X only
+    MOD_CAPSLOCK
+    MOD_NUMLOCK
+    MOD_SCROLLLOCK
+    MOD_ACCEL       Equivalent to MOD_CTRL, or MOD_COMMAND on Mac OS X.
+
+For example, to test if the shift key is held down::
+
+    if modifiers & MOD_SHIFT:
+        pass
+
+Unlike the corresponding key symbols, it is not possible to determine whether
+the left or right modifier is held down (though you could emulate this
+behaviour by keeping track of the key states yourself).
+
+User-defined key symbols
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+pyglet does not define key symbols for every keyboard ever made.  For example,
+non-Latin languages will have many keys not recognised by pyglet (however,
+their Unicode representations will still be valid, see
+:ref:`guide_text-and-motion-events`).
+Even English keyboards often have additional so-called "OEM" keys
+added by the manufacturer, which might be labelled "Media", "Volume" or
+"Shopping", for example.
+
+In these cases pyglet will create a key symbol at runtime based on the
+hardware scancode of the key.  This is guaranteed to be unique for that model
+of keyboard, but may not be consistent across other keyboards with the same
+labelled key.
+
+The best way to use these keys is to record what the user presses after a
+prompt, and then check for that same key symbol.  Many commercial games have
+similar functionality in allowing players to set up their own key bindings.
+
+Remembering key state
+^^^^^^^^^^^^^^^^^^^^^
+
+pyglet provides the convenience class
+:py:class:`~pyglet.window.key.KeyStateHandler` for storing the
+current keyboard state.  This can be pushed onto the event handler stack of
+any window and subsequently queried as a dict::
+
+    from pyglet.window import key
+
+    window = pyglet.window.Window()
+    keys = key.KeyStateHandler()
+    window.push_handlers(keys)
+
+    # Check if the spacebar is currently pressed:
+    if keys[key.SPACE]:
+        pass
+
+.. _guide_text-and-motion-events:
+
+Text and motion events
+----------------------
+
+pyglet decouples the keys that the user presses from the Unicode text that is
+input.  There are several benefits to this:
+
+* The complex task of mapping modifiers and key symbols to Unicode characters
+  is taken care of automatically and correctly.
+* Key repeat is applied to keys held down according to the user's operating
+  system preferences.
+* Dead keys and compose keys are automatically interpreted to produce
+  diacritic marks or combining characters.
+* Keyboard input can be routed via an input palette, for example to input
+  characters from Asian languages.
+* Text input can come from other user-defined sources, such as handwriting or
+  voice recognition.
+
+The actual source of input (i.e., which keys were pressed, or what input
+method was used) should be considered outside of the scope of the application
+-- the operating system provides the necessary services.
+
+When text is entered into a window, the
+:py:meth:`~pyglet.window.Window.on_text` event is fired::
+
+    def on_text(text):
+        pass
+
+The only parameter provided is a Unicode string.
+For keyboard input this will usually be one character long,
+however more complex input methods such as an input palette may
+provide an entire word or phrase at once.
+
+You should always use the :py:meth:`~pyglet.window.Window.on_text`
+event when you need to determine a string from a sequence of keystrokes.
+Conversely, you never use :py:meth:`~pyglet.window.Window.on_text` when you
+require keys to be pressed (for example, to control the movement of the player
+in a game).
+
+Motion events
+^^^^^^^^^^^^^
+
+In addition to entering text, users press keys on the keyboard to navigate
+around text widgets according to well-ingrained conventions.  For example,
+pressing the left arrow key moves the cursor one character to the left.
+
+While you might be tempted to use the
+:py:meth:`~pyglet.window.Window.on_key_press` event to capture these
+events, there are a couple of problems:
+
+* Key repeat events are not generated for
+  :py:meth:`~pyglet.window.Window.on_key_press`, yet users expect
+  that holding down the left arrow key will eventually move the character to
+  the beginning of the line.
+* Different operating systems have different conventions for the behaviour of
+  keys.  For example, on Windows it is customary for the Home key to move the
+  cursor to the beginning of the line, whereas on Mac OS X the same key moves
+  to the beginning of the document.
+
+pyglet windows provide the :py:meth:`~pyglet.window.Window.on_text_motion`
+event, which takes care of these problems by abstracting away the key presses
+and providing your application only with the intended cursor motion::
+
+    def on_text_motion(motion):
+        pass
+
+`motion` is an integer which is a constant defined in
+:py:mod:`pyglet.window.key`. The following table shows the defined text motions
+and their keyboard mapping on each operating system.
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Constant
+          - Behaviour
+          - Windows/Linux
+          - Mac OS X
+        * - ``MOTION_UP``
+          - Move the cursor up
+          - Up
+          - Up
+        * - ``MOTION_DOWN``
+          - Move the cursor down
+          - Down
+          - Down
+        * - ``MOTION_LEFT``
+          - Move the cursor left
+          - Left
+          - Left
+        * - ``MOTION_RIGHT``
+          - Move the cursor right
+          - Right
+          - Right
+        * - ``MOTION_PREVIOUS_WORD``
+          - Move the cursor to the previous word
+          - Ctrl + Left
+          - Option + Left
+        * - ``MOTION_NEXT_WORD``
+          - Move the cursor to the next word
+          - Ctrl + Right
+          - Option + Right
+        * - ``MOTION_BEGINNING_OF_LINE``
+          - Move the cursor to the beginning of the current line
+          - Home
+          - Command + Left
+        * - ``MOTION_END_OF_LINE``
+          - Move the cursor to the end of the current line
+          - End
+          - Command + Right
+        * - ``MOTION_PREVIOUS_PAGE``
+          - Move to the previous page
+          - Page Up
+          - Page Up
+        * - ``MOTION_NEXT_PAGE``
+          - Move to the next page
+          - Page Down
+          - Page Down
+        * - ``MOTION_BEGINNING_OF_FILE``
+          - Move to the beginning of the document
+          - Ctrl + Home
+          - Home
+        * - ``MOTION_END_OF_FILE``
+          - Move to the end of the document
+          - Ctrl + End
+          - End
+        * - ``MOTION_BACKSPACE``
+          - Delete the previous character
+          - Backspace
+          - Backspace
+        * - ``MOTION_DELETE``
+          - Delete the next character, or the current character
+          - Delete
+          - Delete
+
+Keyboard exclusivity
+--------------------
+
+Some keystrokes or key combinations normally bypass applications and are
+handled by the operating system.  Some examples are Alt+Tab (Command+Tab on
+Mac OS X) to switch applications and the keys mapped to Expose on Mac OS X.
+
+You can disable these hot keys and have them behave as ordinary keystrokes for
+your application.  This can be useful if you are developing a kiosk
+application which should not be closed, or a game in which it is possible for
+a user to accidentally press one of these keys.
+
+To enable this mode, call
+:py:meth:`~pyglet.window.Window.set_exclusive_keyboard` for the window on
+which it should apply.  On Mac OS X the dock and menu bar will slide out of
+view while exclusive keyboard is activated.
+
+The following restrictions apply on Windows:
+
+* Most keys are not disabled: a user can still switch away from your
+  application using Ctrl+Escape, Alt+Escape, the Windows key or
+  Ctrl+Alt+Delete.  Only the Alt+Tab combination is disabled.
+
+The following restrictions apply on Mac OS X:
+
+* The power key is not disabled.
+
+Use of this function is not recommended for general release applications or
+games as it violates user-interface conventions.
diff --git a/doc/programming_guide/media.rst b/doc/programming_guide/media.rst
new file mode 100644
index 0000000..255d6c8
--- /dev/null
+++ b/doc/programming_guide/media.rst
@@ -0,0 +1,604 @@
+Sound and video
+===============
+
+pyglet can play many audio and video formats. Audio is played back with
+either OpenAL, XAudio2, DirectSound, or Pulseaudio, permitting hardware-accelerated
+mixing and surround-sound 3D positioning. Video is played into OpenGL
+textures, and so can be easily manipulated in real-time by applications
+and incorporated into 3D environments.
+
+Decoding of compressed audio and video is provided by `FFmpeg`_ v4.X, an
+optional component available for Linux, Windows and Mac OS X. FFmpeg needs
+to be installed separately.
+
+If FFmpeg is not present, pyglet will fall back to reading uncompressed WAV
+files only. This may be sufficient for many applications that require only a
+small number of short sounds, in which case those applications need not
+distribute FFmpeg.
+
+.. _FFmpeg: https://www.ffmpeg.org/download.html
+
+.. _openal.org: https://www.openal.org/downloads
+
+Audio drivers
+-------------
+
+pyglet can use OpenAL, XAudio2, DirectSound or Pulseaudio to play back audio. Only one
+of these drivers can be used in an application. In most cases you won't need
+to concern yourself with choosing a driver, but you can manually select one if
+desired. This must be done before the :py:mod:`pyglet.media` module is loaded.
+The available drivers depend on your operating system:
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Windows
+          - Mac OS X
+          - Linux
+        * - OpenAL [#openalf]_
+          - OpenAL
+          - OpenAL [#openalf]_
+        * - DirectSound
+          -
+          -
+        * -
+          -
+          - Pulseaudio
+
+The audio driver can be set through the ``audio`` key of the
+:py:data:`pyglet.options` dictionary. For example::
+
+    pyglet.options['audio'] = ('openal', 'pulse', 'directsound', 'silent')
+
+This tells pyglet to try using the OpenAL driver first, and if not available
+to try Pulseaudio and DirectSound in that order. If all else fails, no driver
+will be instantiated. The ``audio`` option can be a list of any of these
+strings, giving the preference order for each driver:
+
+    .. list-table::
+        :header-rows: 1
+
+        * - String
+          - Audio driver
+        * - ``openal``
+          - OpenAL
+        * - ``directsound``
+          - DirectSound
+        * - ``xaudio2``
+          - XAudio2
+        * - ``pulse``
+          - Pulseaudio
+        * - ``silent``
+          - No audio output
+
+You must set the ``audio`` option before importing :mod:`pyglet.media`.
+You  can alternatively set it through an environment variable;
+see :ref:`guide_environment-settings`.
+
+The following sections describe the requirements and limitations of each audio
+driver.
+
+XAudio2
+^^^^^^^^^^^
+XAudio2 is only available on Windows Vista and above and is the replacement of
+DirectSound. This provides hardware accelerated audio support for newer operating
+systems.
+
+Note that in some stripped down versions of Windows 10, XAudio2 may not be available
+until the required DLL's are installed.
+
+DirectSound
+^^^^^^^^^^^
+
+DirectSound is available only on Windows, and is installed by default.
+pyglet uses only DirectX 7 features. On Windows Vista, DirectSound does not
+support hardware audio mixing or surround sound.
+
+OpenAL
+^^^^^^
+
+OpenAL is included with Mac OS X. Windows users can download a generic driver
+from `openal.org`_, or from their sound device's manufacturer. Most Linux
+distributions will have OpenAL available in the repositories for download.
+For example, Ubuntu users can ``apt install libopenal1``.
+
+Pulse
+^^^^^
+
+Pulseaudio has become the standard Linux audio implementation over the past
+few years, and is installed by default with most modern Linux distributions.
+Pulseaudio does not support positional audio, and is limited to stereo. It
+is recommended to use OpenAL if positional audio is required.
+
+.. [#openalf] OpenAL is not installed by default on Windows, nor in many Linux
+    distributions. It can be downloaded separately from your audio device
+    manufacturer or `openal.org <https://www.openal.org/downloads>`_
+
+Supported media types
+---------------------
+
+Windows and Linux both support a limited amount of compressed audio types, without
+the need for FFmpeg. While FFmpeg supports a large array of formats and codecs, it
+may be an unnecessarily large dependency when simple audio playback is needed on
+these operating systems.
+
+These formats are supported natively under the following systems and codecs:
+
+Windows Media Foundation
+^^^^^^^^^^^^^^^^^^^^^^^^
+Supported on Windows operating systems.
+
+The following are supported on **Windows Vista and above**:
+
+* MP3
+* WMA
+* ASF
+* SAMI/SMI
+
+The following are supported on **Windows 7 and above**:
+
+* 3G2/3GP/3GP2/3GP
+* AAC/ADTS
+* AVI
+* M4A/M4V/MOV/MP4
+
+The following is undocumented but known to work on **Windows 10**:
+
+* FLAC
+
+Please note that any video playback done through WMF is limited in codec
+support and is **not** hardware accelerated. It should only be used for simple
+or small videos. FFmpeg is recommended for all other purposes.
+
+GStreamer
+^^^^^^^^^
+Supported on Linux operating systems that have the GStreamer installed. Please note that the
+associated Python packages for gobject & gst are also required. This varies by distribution,
+but will often already be installed along with GStreamer.
+
+* MP3
+* FLAC
+* OGG
+* M4A
+
+PyOgg
+^^^^^
+Supported on Windows, Linux, and Mac operating systems.
+
+PyOgg is a lightweight Python library that provides Python bindings for Opus, Vorbis,
+and FLAC codecs.
+
+Pyglet now provides a wrapper to support PyOgg. Since not all operating systems
+can decode the same audio formats natively, it can often be a hassle to choose
+an audio format that is truely cross platform with a small footprint. This wrapper
+was created to help with that issue.
+
+Supports the following formats:
+
+* OGG
+* FLAC
+* OPUS
+
+Refer to their installation guide found here: https://pyogg.readthedocs.io/en/latest/installation.html
+
+FFmpeg
+^^^^^^
+FFmpeg requires an external dependency, please see installation instructions
+in the next section below.
+
+With FFmpeg, many common and less-common formats are supported. Due to the
+large number of combinations of audio and video codecs, options, and container
+formats, it is difficult to provide a complete yet useful list. Some of the
+supported audio formats are:
+
+* AU
+* MP2
+* MP3
+* OGG/Vorbis
+* WAV
+* WMA
+
+Some of the supported video formats are:
+
+* AVI
+* DivX
+* H.263
+* H.264
+* MPEG
+* MPEG-2
+* OGG/Theora
+* Xvid
+* WMV
+* Webm
+
+For a complete list, see the FFmpeg sources. Otherwise, it is probably simpler
+to try playing back your target file with the ``media_player.py`` example.
+
+New versions of FFmpeg as they are released may support additional formats, or
+fix errors in the current implementation. Currently a C bindings was written
+with ctypes using FFmpeg v4.X. This means that this version of pyglet will
+support all FFmpeg binaries with the major version set to 4.
+
+FFmpeg installation
+-------------------
+
+You can install FFmpeg for your platform by following the instructions found
+in the `FFmpeg download <https://www.ffmpeg.org/download.html>`_ page. You must
+choose the shared build for the targeted OS with the architecture similar to
+the Python interpreter.
+
+This means that the major version must be 4.X. All minor versions are
+supported. Choose the correct architecture depending on the targeted
+**Python interpreter**. If you're shipping your project with a 32 bits
+interpreter, you must download the 32 bits shared binaries.
+
+On Windows, the usual error message when the wrong architecture was downloaded
+is::
+
+    WindowsError: [Error 193] %1 is not a valid Win32 application
+
+Finally make sure you download the **shared** builds, not the static or the
+dev builds.
+
+For Mac OS and Linux, the library is usually already installed system-wide.
+For Windows users, it's not recommended to install the library in one of the
+windows sub-folders.
+
+Instead we recommend to use the :py:data:`pyglet.options`
+``search_local_libs``::
+
+    import pyglet
+    pyglet.options['search_local_libs'] = True
+
+This will allow pyglet to find the FFmpeg binaries in the ``lib`` sub-folder
+located in your running script folder.
+
+Another solution is to manipulate the environment variable. On Windows you can
+add the dll location to the PATH::
+
+    os.environ["PATH"] += "path/to/ffmpeg"
+
+For Linux and Mac OS::
+
+    os.environ["LD_LIBRARY_PATH"] += ":" + "path/to/ffmpeg"
+
+Loading media
+-------------
+
+Audio and video files are loaded in the same way, using the
+:py:func:`pyglet.media.load` function, providing a filename::
+
+    source = pyglet.media.load('explosion.wav')
+
+If the media file is bundled with the application, consider using the
+:py:mod:`~pyglet.resource` module (see :ref:`guide_resources`).
+
+The result of loading a media file is a
+:py:class:`~pyglet.media.Source` object. This object provides useful
+information about the type of media encoded in the file, and serves as an
+opaque object used for playing back the file (described in the next section).
+
+The :py:func:`~pyglet.media.load` function will raise a
+:py:class:`~pyglet.media.exceptions.MediaException` if the format is unknown.
+``IOError`` may also be raised if the file could not be read from disk.
+Future versions of pyglet will also support reading from arbitrary file-like
+objects, however a valid filename must currently be given.
+
+The length of the media file is given by the
+:py:class:`~pyglet.media.Source.duration` property, which returns the media's
+length in seconds.
+
+Audio metadata is provided in the source's
+:py:attr:`~pyglet.media.Source.audio_format` attribute, which is ``None`` for
+silent videos. This metadata is not generally useful to applications. See
+the :py:class:`~pyglet.media.AudioFormat` class documentation for details.
+
+Video metadata is provided in the source's
+:py:attr:`~pyglet.media.Source.video_format` attribute, which is ``None`` for
+audio files. It is recommended that this attribute is checked before
+attempting play back a video file -- if a movie file has a readable audio
+track but unknown video format it will appear as an audio file.
+
+You can use the video metadata, described in a
+:py:class:`~pyglet.media.VideoFormat` object, to set up display of the video
+before beginning playback. The attributes are as follows:
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Attribute
+          - Description
+        * - ``width``, ``height``
+          - Width and height of the video image, in pixels.
+        * - ``sample_aspect``
+          - The aspect ratio of each video pixel.
+
+You must take care to apply the sample aspect ratio to the video image size
+for display purposes. The following code determines the display size for a
+given video format::
+
+    def get_video_size(width, height, sample_aspect):
+        if sample_aspect > 1.:
+            return width * sample_aspect, height
+        elif sample_aspect < 1.:
+            return width, height / sample_aspect
+        else:
+            return width, height
+
+Media files are not normally read entirely from disk; instead, they are
+streamed into the decoder, and then into the audio buffers and video memory
+only when needed. This reduces the startup time of loading a file and reduces
+the memory requirements of the application.
+
+However, there are times when it is desirable to completely decode an audio
+file in memory first. For example, a sound that will be played many times
+(such as a bullet or explosion) should only be decoded once. You can instruct
+pyglet to completely decode an audio file into memory at load time::
+
+    explosion = pyglet.media.load('explosion.wav', streaming=False)
+
+The resulting source is an instance of :class:`~pyglet.media.StaticSource`,
+which provides the same interface as a :class:`~pyglet.media.StreamingSource`.
+You can also construct a :class:`~pyglet.media.StaticSource` directly from an
+already- loaded :class:`~pyglet.media.Source`::
+
+    explosion = pyglet.media.StaticSource(pyglet.media.load('explosion.wav'))
+
+Audio Synthesis
+---------------
+
+In addition to loading audio files, the :py:mod:`pyglet.media.synthesis`
+module is available for simple audio synthesis. There are several basic
+waveforms available:
+
+* :py:class:`~pyglet.media.synthesis.Sine`
+* :py:class:`~pyglet.media.synthesis.Sawtooth`
+* :py:class:`~pyglet.media.synthesis.Square`
+* :py:class:`~pyglet.media.synthesis.FM`
+* :py:class:`~pyglet.media.synthesis.Silence`
+* :py:class:`~pyglet.media.synthesis.WhiteNoise`
+* :py:class:`~pyglet.media.synthesis.Digitar`
+
+The module documentation for each will provide more information on
+constructing them, but at a minimum you will need to specify the duration.
+You will also want to set the audio frequency (most waveforms will default
+to 440Hz). Some waveforms, such as the FM, have additional parameters.
+
+For shaping the waveforms, several simple envelopes are available.
+These envelopes affect the amplitude (volume), and can make for more
+natural sounding tones. You first create an envelope instance,
+and then pass it into the constructor of any of the above waveforms.
+The same envelope instance can be passed to any number of waveforms,
+reducing duplicate code when creating multiple sounds.
+If no envelope is used, all waveforms will default to the FlatEnvelope
+of maximum volume, which esentially has no effect on the sound.
+Check the module documentation of each Envelope to see which parameters
+are available.
+
+* :py:class:`~pyglet.media.synthesis.FlatEnvelope`
+* :py:class:`~pyglet.media.synthesis.LinearDecayEnvelope`
+* :py:class:`~pyglet.media.synthesis.ADSREnvelope`
+* :py:class:`~pyglet.media.synthesis.TremoloEnvelope`
+
+An example of creating an envelope and waveforms::
+
+    adsr = pyglet.media.synthesis.ADSREnvelope(0.05, 0.2, 0.1)
+
+    saw = pyglet.media.synthesis.Sawtooth(duration=1.0, frequency=220, envelope=adsr)
+    fm = pyglet.media.synthesis.FM(3, carrier=440, modulator=2, mod_index=22, envelope=adsr)
+
+The waveforms you create with the synthesis module can be played like any
+other loaded sound. See the next sections for more detail on playback.
+
+Simple audio playback
+---------------------
+
+Many applications, especially games, need to play sounds in their entirety
+without needing to keep track of them. For example, a sound needs to be
+played when the player's space ship explodes, but this sound never needs to
+have its volume adjusted, or be rewound, or interrupted.
+
+pyglet provides a simple interface for this kind of use-case. Call the
+:meth:`~pyglet.media.Source.play` method of any :class:`~pyglet.media.Source`
+to play it immediately and completely::
+
+    explosion = pyglet.media.load('explosion.wav', streaming=False)
+    explosion.play()
+
+You can call :py:meth:`~pyglet.media.Source.play` on any
+:py:class:`~pyglet.media.Source`, not just
+:py:class:`~pyglet.media.StaticSource`.
+
+The return value of :py:meth:`~pyglet.media.Source.play` is a
+:py:class:`~pyglet.media.player.Player`, which can either be
+discarded, or retained to maintain control over the sound's playback.
+
+Controlling playback
+--------------------
+
+You can implement many functions common to a media player using the
+:py:class:`~pyglet.media.player.Player`
+class. Use of this class is also necessary for video playback. There are no
+parameters to its construction::
+
+    player = pyglet.media.Player()
+
+A player will play any source that is *queued* on it. Any number of sources
+can be queued on a single player, but once queued, a source can never be
+dequeued (until it is removed automatically once complete). The main use of
+this queueing mechanism is to facilitate "gapless" transitions between
+playback of media files.
+
+The :py:meth:`~pyglet.media.player.Player.queue` method is used to queue
+a media on the player - a :py:class:`~pyglet.media.StreamingSource` or a
+:py:class:`~pyglet.media.StaticSource`. Either you pass one instance, or you
+can also pass an iterable of sources. This provides great flexibility. For
+instance, you could create a generator which takes care of the logic about
+what music to play::
+
+    def my_playlist():
+       yield intro
+       while game_is_running():
+          yield main_theme
+       yield ending
+
+    player.queue(my_playlist())
+
+When the game ends, you will still need to call on the player::
+
+    player.next_source()
+
+The generator will pass the ``ending`` media to the player.
+
+A :py:class:`~pyglet.media.StreamingSource` can only ever be queued on one
+player, and only once on that player. :py:class:`~pyglet.media.StaticSource`
+objects can be queued any number of times on any number of players. Recall
+that a :py:class:`~pyglet.media.StaticSource` can be created by passing
+``streaming=False`` to the :py:func:`pyglet.media.load` method.
+
+In the following example, two sounds are queued onto a player::
+
+    player.queue(source1)
+    player.queue(source2)
+
+Playback begins with the player's :py:meth:`~pyglet.media.Player.play` method
+is called::
+
+    player.play()
+
+Standard controls for controlling playback are provided by these methods:
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Method
+          - Description
+        * - :py:meth:`~pyglet.media.Player.play`
+          - Begin or resume playback of the current source.
+        * - :py:meth:`~pyglet.media.Player.pause`
+          - Pause playback of the current source.
+        * - :py:meth:`~pyglet.media.Player.next_source`
+          - Dequeue the current source and move to the next one immediately.
+        * - :py:meth:`~pyglet.media.Player.seek`
+          - Seek to a specific time within the current source.
+
+Note that there is no `stop` method. If you do not need to resume playback,
+simply pause playback and discard the player and source objects. Using the
+:meth:`~pyglet.media.Player.next_source` method does not guarantee gapless
+playback.
+
+There are several properties that describe the player's current state:
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Property
+          - Description
+        * - :py:attr:`~pyglet.media.Player.time`
+          - The current playback position within the current source, in
+            seconds. This is read-only (but see the :py:meth:`~pyglet.media.Player.seek` method).
+        * - :py:attr:`~pyglet.media.Player.playing`
+          - True if the player is currently playing, False if there are no
+            sources queued or the player is paused. This is read-only (but
+            see the :py:meth:`~pyglet.media.Player.pause` and :py:meth:`~pyglet.media.Player.play` methods).
+        * - :py:attr:`~pyglet.media.Player.source`
+          - A reference to the current source being played. This is
+            read-only (but see the :py:meth:`~pyglet.media.Player.queue` method).
+        * - :py:attr:`~pyglet.media.Player.volume`
+          - The audio level, expressed as a float from 0 (mute) to 1 (normal
+            volume). This can be set at any time.
+        * - :py:attr:`~pyglet.media.player.Player.loop`
+          - ``True`` if the current source should be repeated when reaching
+            the end. If set to ``False``, playback will continue to the next
+            queued source.
+
+When a player reaches the end of the current source, by default it will move
+immediately to the next queued source. If there are no more sources, playback
+stops until another source is queued. The :class:`~pyglet.media.player.Player`
+has a :py:attr:`~pyglet.media.player.Player.loop` attribute which determines
+the player behaviour when the current source reaches the end. If
+:py:attr:`~pyglet.media.player.Player.loop` is ``False`` (default) the
+:class:`~pyglet.media.player.Player` starts to play the next queued source.
+Otherwise the :class:`~pyglet.media.player.Player` re-plays the current source
+until either :py:attr:`~pyglet.media.player.Player.loop` is set to ``False``
+or :py:meth:`~pyglet.media.Player.next_source` is called.
+
+You can change the :py:attr:`~pyglet.media.player.Player.loop` attribute at
+any time,  but be aware that unless sufficient time is given for the future
+data to be  decoded and buffered there may be a stutter or gap in playback.
+If set well  in advance of the end of the source (say, several seconds), there
+will be no  disruption.
+
+Gapless playback
+----------------
+
+To play back multiple similar sources without any audible gaps,
+:py:class:`~pyglet.media.SourceGroup` is provided.
+A :py:class:`~pyglet.media.SourceGroup` can only contain media sources
+with identical audio or video format. First create an instance of
+:py:class:`~pyglet.media.SourceGroup`, and then add all desired additional
+sources with the :func:`~pyglet.media.SourceGroup.add` method.
+Afterwards, you can queue the :py:class:`~pyglet.media.SourceGroup`
+on a Player as if it was a single source.
+
+Incorporating video
+-------------------
+
+When a :py:class:`~pyglet.media.player.Player` is playing back a source with
+video, use the :attr:`~pyglet.media.Player.texture` property to obtain the
+video frame image. This can be used to display the current video image
+syncronised with the audio track, for example::
+
+    @window.event
+    def on_draw():
+        player.texture.blit(0, 0)
+
+The texture is an instance of :class:`pyglet.image.Texture`, with an internal
+format of either ``GL_TEXTURE_2D`` or ``GL_TEXTURE_RECTANGLE_ARB``. While the
+texture will typically be created only once and subsequentally updated each
+frame, you should make no such assumption in your application -- future
+versions of pyglet may use multiple texture objects.
+
+Positional audio
+----------------
+
+pyglet includes features for positioning sound within a 3D space. This is
+particularly effective with a surround-sound setup, but is also applicable to
+stereo systems.
+
+A :py:class:`~pyglet.media.player.Player` in pyglet has an associated position
+in 3D space -- that is, it is equivalent to an OpenAL "source". The properties
+for setting these parameters are described in more detail in the API
+documentation; see for example :py:attr:`~pyglet.media.Player.position` and
+:py:attr:`~pyglet.media.Player.pitch`.
+
+A "listener" object is provided by the audio driver. To obtain the listener
+for the current audio driver::
+
+    pyglet.media.get_audio_driver().get_listener()
+
+This provides similar properties such as
+:py:attr:`~pyglet.media.listener.AbstractListener.position`,
+:py:attr:`~pyglet.media.listener.AbstractListener.forward_orientation` and
+:py:attr:`~pyglet.media.listener.AbstractListener.up_orientation` that
+describe the  position of the user in 3D space.
+
+Note that only mono sounds can be positioned. Stereo sounds will play back as
+normal, and only their volume and pitch properties will affect the sound.
+
+Ticking the clock
+-----------------
+
+If you are using pyglet's media libraries outside of a pyglet app, you will need 
+to use some kind of loop to tick the pyglet clock periodically (perhaps every 
+200ms or so), otherwise only the first small sample of media will be played:
+
+    pyglet.clock.tick()
+
+If you wish to have a media source loop continuously (`player.loop = True`) you will
+also need to ensure Pyglet's events are dispatched inside your loop:
+
+    pyglet.app.platform_event_loop.dispatch_posted_events()
+
+If you are inside a pyglet app then calling `pyglet.app.run()` takes care of 
+all this for you.
diff --git a/doc/programming_guide/mouse.rst b/doc/programming_guide/mouse.rst
new file mode 100644
index 0000000..424d76c
--- /dev/null
+++ b/doc/programming_guide/mouse.rst
@@ -0,0 +1,268 @@
+Working with the mouse
+======================
+
+All pyglet windows can receive input from a 3 button mouse with a
+2 dimensional scroll wheel.  The mouse pointer is typically drawn by the
+operating system, but you can override this and request either a different
+cursor shape or provide your own image or animation.
+
+Mouse events
+------------
+
+All mouse events are dispatched by the window which receives the event from
+the operating system.  Typically this is the window over which the mouse
+cursor is, however mouse exclusivity and drag operations mean this is not
+always the case.
+
+The coordinate space for the mouse pointer's location is relative to the
+bottom-left corner of the window, with increasing Y values approaching the top
+of the screen (note that this is "upside-down" compared with many other
+windowing toolkits, but is consistent with the default OpenGL projection in
+pyglet).
+
+.. figure:: img/mouse_coordinates.png
+
+    The coordinate space for the mouse pointer.
+
+The most basic mouse event is :py:meth:`~pyglet.window.Window.on_mouse_motion`
+which is dispatched every time the mouse moves::
+
+    def on_mouse_motion(x, y, dx, dy):
+        pass
+
+The `x` and `y` parameters give the coordinates of the mouse pointer, relative
+to the bottom-left corner of the window.
+
+The event is dispatched every time the operating system registers a mouse
+movement.  This is not necessarily once for every pixel moved -- the operating
+system typically samples the mouse at a fixed frequency, and it is easy to
+move the mouse faster than this.  Conversely, if your application is not
+processing events fast enough you may find that several queued-up mouse events
+are dispatched in a single :py:meth:`~pyglet.window.Window.dispatch_events`
+call. There is no need to concern yourself with either of these issues;
+the latter rarely causes problems, and the former can not be avoided.
+
+Many games are not concerned with the actual position of the mouse cursor,
+and only need to know in which direction the mouse has moved.  For example,
+the mouse in a first-person game typically controls the direction the player
+looks, but the mouse pointer itself is not displayed.
+
+The `dx` and `dy` parameters are for this purpose: they give the distance the
+mouse travelled along each axis to get to its present position.  This can be
+computed naively by storing the previous `x` and `y` parameters after every
+mouse event, but besides being tiresome to code, it does not take into account
+the effects of other obscuring windows.  It is best to use the `dx` and `dy`
+parameters instead.
+
+The following events are dispatched when a mouse button is pressed or
+released, or the mouse is moved while any button is held down::
+
+    def on_mouse_press(x, y, button, modifiers):
+        pass
+
+    def on_mouse_release(x, y, button, modifiers):
+        pass
+
+    def on_mouse_drag(x, y, dx, dy, buttons, modifiers):
+        pass
+
+The `x`, `y`, `dx` and `dy` parameters are as for the
+:py:meth:`~pyglet.window.Window.on_mouse_motion` event.
+The press and release events do not require `dx` and `dy` parameters as they
+would be zero in this case.  The `modifiers` parameter is as for the keyboard
+events, see :ref:`guide_working-with-the-keyboard`.
+
+The `button` parameter signifies which mouse button was pressed, and is one of
+the following constants::
+
+    pyglet.window.mouse.LEFT
+    pyglet.window.mouse.MIDDLE
+    pyglet.window.mouse.RIGHT
+
+The `buttons` parameter in :py:meth:`~pyglet.window.Window.on_mouse_drag`
+is a bitwise combination of all the mouse buttons currently held down.
+For example, to test if the user is performing a drag gesture with the
+left button::
+
+    from pyglet.window import mouse
+
+    def on_mouse_drag(x, y, dx, dy, buttons, modifiers):
+        if buttons & mouse.LEFT:
+            pass
+
+When the user begins a drag operation (i.e., pressing and holding a mouse
+button and then moving the mouse), the window in which they began the drag
+will continue to receive the :py:meth:`~pyglet.window.Window.on_mouse_drag`
+event as long as the button is held down.
+This is true even if the mouse leaves the window.
+You generally do not need to handle this specially: it is a convention
+among all operating systems that dragging is a gesture rather than a direct
+manipulation of the user interface widget.
+
+There are events for when the mouse enters or leaves a window::
+
+    def on_mouse_enter(x, y):
+        pass
+
+    def on_mouse_leave(x, y):
+        pass
+
+The coordinates for :py:meth:`~pyglet.window.Window.on_mouse_leave` will
+lie outside of your window. These events are not dispatched while a drag
+operation is taking place.
+
+The mouse scroll wheel generates the
+:py:meth:`~pyglet.window.Window.on_mouse_scroll` event::
+
+    def on_mouse_scroll(x, y, scroll_x, scroll_y):
+        pass
+
+The `scroll_y` parameter gives the number of "clicks" the wheel moved, with
+positive numbers indicating the wheel was pushed forward.  The `scroll_x`
+parameter is 0 for most mice, however some new mice such as the Apple Mighty
+Mouse use a ball instead of a wheel; the `scroll_x` parameter gives the
+horizontal movement in this case.  The scale of these numbers is not known; it
+is typically set by the user in their operating system preferences.
+
+Changing the mouse cursor
+-------------------------
+
+The mouse cursor can be set to one of the operating system cursors, a custom
+image, or hidden completely.  The change to the cursor will be applicable only
+to the window you make the change to.  To hide the mouse cursor, call
+:py:meth:`~pyglet.window.Window.set_mouse_visible`::
+
+    win = pyglet.window.Window()
+    win.set_mouse_visible(False)
+
+This can be useful if the mouse would obscure text that the user is typing.
+If you are hiding the mouse cursor for use in a game environment, consider
+making the mouse exclusive instead; see :ref:`guide_mouse-exclusivity`, below.
+
+Use :py:meth:`~pyglet.window.Window.set_mouse_cursor` to change the appearance
+of the mouse cursor. A mouse cursor is an instance of
+:py:class:`~pyglet.window.MouseCursor`. You can obtain the operating
+system-defined cursors with
+:py:meth:`~pyglet.window.Window.get_system_mouse_cursor`::
+
+    cursor = win.get_system_mouse_cursor(win.CURSOR_HELP)
+    win.set_mouse_cursor(cursor)
+
+The cursors that pyglet defines are listed below, along with their typical
+appearance on Windows and Mac OS X.  The pointer image on Linux is dependent
+on the window manager.
+
+    .. list-table::
+        :header-rows: 1
+        :stub-columns: 1
+        :class: images
+
+        * - Constant
+          - Windows XP
+          - Mac OS X
+        * - `CURSOR_DEFAULT`
+          - .. image:: img/cursor_win_default.png
+          - .. image:: img/cursor_mac_default.png
+        * - `CURSOR_CROSSHAIR`
+          - .. image:: img/cursor_win_crosshair.png
+          - .. image:: img/cursor_mac_crosshair.png
+        * - `CURSOR_HAND`
+          - .. image:: img/cursor_win_hand.png
+          - .. image:: img/cursor_mac_hand.png
+        * - `CURSOR_HELP`
+          - .. image:: img/cursor_win_help.png
+          - .. image:: img/cursor_mac_default.png
+        * - `CURSOR_NO`
+          - .. image:: img/cursor_win_no.png
+          - .. image:: img/cursor_mac_no.png
+        * - `CURSOR_SIZE`
+          - .. image:: img/cursor_win_size.png
+          - .. image:: img/cursor_mac_default.png
+        * - `CURSOR_SIZE_DOWN`
+          - .. image:: img/cursor_win_size_up_down.png
+          - .. image:: img/cursor_mac_size_down.png
+        * - `CURSOR_SIZE_DOWN_LEFT`
+          - .. image:: img/cursor_win_size_nesw.png
+          - .. image:: img/cursor_mac_default.png
+        * - `CURSOR_SIZE_DOWN_RIGHT`
+          - .. image:: img/cursor_win_size_nwse.png
+          - .. image:: img/cursor_mac_default.png
+        * - `CURSOR_SIZE_LEFT`
+          - .. image:: img/cursor_win_size_left_right.png
+          - .. image:: img/cursor_mac_size_left.png
+        * - `CURSOR_SIZE_LEFT_RIGHT`
+          - .. image:: img/cursor_win_size_left_right.png
+          - .. image:: img/cursor_mac_size_left_right.png
+        * - `CURSOR_SIZE_RIGHT`
+          - .. image:: img/cursor_win_size_left_right.png
+          - .. image:: img/cursor_mac_size_right.png
+        * - `CURSOR_SIZE_UP`
+          - .. image:: img/cursor_win_size_up_down.png
+          - .. image:: img/cursor_mac_size_up.png
+        * - `CURSOR_SIZE_UP_DOWN`
+          - .. image:: img/cursor_win_size_up_down.png
+          - .. image:: img/cursor_mac_size_up_down.png
+        * - `CURSOR_SIZE_UP_LEFT`
+          - .. image:: img/cursor_win_size_nwse.png
+          - .. image:: img/cursor_mac_default.png
+        * - `CURSOR_SIZE_UP_RIGHT`
+          - .. image:: img/cursor_win_size_nesw.png
+          - .. image:: img/cursor_mac_default.png
+        * - `CURSOR_TEXT`
+          - .. image:: img/cursor_win_text.png
+          - .. image:: img/cursor_mac_text.png
+        * - `CURSOR_WAIT`
+          - .. image:: img/cursor_win_wait.png
+          - .. image:: img/cursor_mac_wait.png
+        * - `CURSOR_WAIT_ARROW`
+          - .. image:: img/cursor_win_wait_arrow.png
+          - .. image:: img/cursor_mac_default.png
+
+Alternatively, you can use your own image as the mouse cursor.  Use
+:py:func:`pyglet.image.load` to load the image, then create an
+:py:class:`~pyglet.window.ImageMouseCursor` with
+the image and "hot-spot" of the cursor.  The hot-spot is the point of the
+image that corresponds to the actual pointer location on screen, for example,
+the point of the arrow::
+
+    image = pyglet.image.load('cursor.png')
+    cursor = pyglet.window.ImageMouseCursor(image, 16, 8)
+    win.set_mouse_cursor(cursor)
+
+You can even render a mouse cursor directly with OpenGL.  You could draw a
+3-dimensional cursor, or a particle trail, for example.  To do this, subclass
+:py:class:`~pyglet.window.MouseCursor` and implement your own draw method.
+The draw method will be called with the default pyglet window projection,
+even if you are using another projection in the rest of your application.
+
+.. _guide_mouse-exclusivity:
+
+Mouse exclusivity
+-----------------
+
+It is possible to take complete control of the mouse for your own application,
+preventing it being used to activate other applications.  This is most useful
+for immersive games such as first-person shooters.
+
+When you enable mouse-exclusive mode, the mouse cursor is no longer available.
+It is not merely hidden -- no amount of mouse movement will make it leave your
+application.  Because there is no longer a mouse cursor, the `x` and `y`
+parameters of the mouse events are meaningless; you should use only the `dx`
+and `dy` parameters to determine how the mouse was moved.
+
+Activate mouse exclusive mode with
+:py:meth:`~pyglet.window.Window.set_exclusive_mouse`::
+
+    win = pyglet.window.Window()
+    win.set_exclusive_mouse(True)
+
+You should activate mouse exclusive mode even if your window is full-screen:
+it will prevent the window "hitting" the edges of the screen, and behave
+correctly in multi-monitor setups (a common problem with commercial
+full-screen games is that the mouse is only hidden, meaning it can
+accidentally travel onto the other monitor where applications are still
+visible).
+
+Note that on Linux setting exclusive mouse also disables Alt+Tab and other
+hotkeys for switching applications.  No workaround for this has yet been
+discovered.
diff --git a/doc/programming_guide/options.rst b/doc/programming_guide/options.rst
new file mode 100644
index 0000000..bc976b7
--- /dev/null
+++ b/doc/programming_guide/options.rst
@@ -0,0 +1,5 @@
+pyglet options
+==============
+
+.. automodule:: pyglet
+   :members:
diff --git a/doc/programming_guide/quickstart.rst b/doc/programming_guide/quickstart.rst
new file mode 100644
index 0000000..8289648
--- /dev/null
+++ b/doc/programming_guide/quickstart.rst
@@ -0,0 +1,223 @@
+.. _quickstart:
+
+Writing a pyglet application
+============================
+
+Getting started with a new library or framework can be daunting, especially
+when presented with a large amount of reference material to read.
+This chapter gives a very quick introduction to pyglet without going into
+too much detail.
+
+Hello, World
+------------
+
+We'll begin with the requisite "Hello, World" introduction. This program will
+open a window with some text in it and wait to be closed. You can find the
+entire program in the `examples/programming_guide/hello_world.py` file.
+
+Begin by importing the :mod:`pyglet` package::
+
+    import pyglet
+
+Create a :class:`pyglet.window.Window` by calling its default constructor.
+The  window will be visible as soon as it's created, and will have reasonable
+default  values for all its parameters::
+
+    window = pyglet.window.Window()
+
+To display the text, we'll create a :class:`~pyglet.text.Label`. Keyword
+arguments are used to set the font, position and anchorage of the label::
+
+    label = pyglet.text.Label('Hello, world',
+                              font_name='Times New Roman',
+                              font_size=36,
+                              x=window.width//2, y=window.height//2,
+                              anchor_x='center', anchor_y='center')
+
+An :meth:`~pyglet.window.Window.on_draw` event is dispatched to the window
+to give it a chance to redraw its contents.  pyglet provides several ways to
+attach event handlers to objects; a simple way is to use a decorator::
+
+    @window.event
+    def on_draw():
+        window.clear()
+        label.draw()
+
+Within the :meth:`~pyglet.window.Window.on_draw` handler the window is cleared
+to the default background color (black), and the label is drawn.
+
+Finally, call::
+
+    pyglet.app.run()
+
+This will enter pyglet's default event loop, and let pyglet respond to
+application events such as the mouse and keyboard.
+Your event handlers will now be called as required, and the
+:func:`~pyglet.app.run` method will return only when all application
+windows have been closed.
+
+If you are coming from another library, you may be used to writing your
+own event loop. This is possible to do with pyglet as well, but it is
+generally discouraged; see :ref:`programming-guide-eventloop` for details.
+
+Image viewer
+------------
+
+Most games and applications will need to load and display images on the
+screen. In this example we'll load an image from the application's
+directory and display it within the window::
+
+    import pyglet
+
+    window = pyglet.window.Window()
+    image = pyglet.resource.image('kitten.jpg')
+
+    @window.event
+    def on_draw():
+        window.clear()
+        image.blit(0, 0)
+
+    pyglet.app.run()
+
+We used the :func:`~pyglet.resource.image` function of :mod:`pyglet.resource`
+to load the image, which automatically locates the file relative to the source
+file (rather than the working directory).  To load an image not bundled with
+the application (for example, specified on the command line), you would use
+:func:`pyglet.image.load`.
+
+The :meth:`~pyglet.image.AbstractImage.blit` method draws the image.  The
+arguments ``(0, 0)`` tell pyglet to draw the image at pixel coordinates 0,
+0 in the window (the lower-left corner).
+
+The complete code for this example is located in
+`examples/programming_guide/image_viewer.py`.
+
+Handling mouse and keyboard events
+----------------------------------
+
+So far the only event used is the :meth:`~pyglet.window.Window.on_draw`
+event.  To react to keyboard and mouse events, it's necessary to write and
+attach event handlers for these events as well::
+
+    import pyglet
+
+    window = pyglet.window.Window()
+
+    @window.event
+    def on_key_press(symbol, modifiers):
+        print('A key was pressed')
+
+    @window.event
+    def on_draw():
+        window.clear()
+
+    pyglet.app.run()
+
+Keyboard events have two parameters: the virtual key `symbol` that was
+pressed, and a bitwise combination of any `modifiers` that are present (for
+example, the ``CTRL`` and ``SHIFT`` keys).
+
+The key symbols are defined in :mod:`pyglet.window.key`::
+
+    from pyglet.window import key
+
+    @window.event
+    def on_key_press(symbol, modifiers):
+        if symbol == key.A:
+            print('The "A" key was pressed.')
+        elif symbol == key.LEFT:
+            print('The left arrow key was pressed.')
+        elif symbol == key.ENTER:
+            print('The enter key was pressed.')
+
+See the :mod:`pyglet.window.key` documentation for a complete list
+of key symbols.
+
+Mouse events are handled in a similar way::
+
+    from pyglet.window import mouse
+
+    @window.event
+    def on_mouse_press(x, y, button, modifiers):
+        if button == mouse.LEFT:
+            print('The left mouse button was pressed.')
+
+The ``x`` and ``y`` parameters give the position of the mouse when the button
+was pressed, relative to the lower-left corner of the window.
+
+There are more than 20 event types that you can handle on a window. An easy
+way to find the event names and parameters you need is to add the following
+lines to your program::
+
+    event_logger = pyglet.window.event.WindowEventLogger()
+    window.push_handlers(event_logger)
+
+This will cause all events received on the window to be printed to the
+console.
+
+An example program using keyboard and mouse events is in
+`examples/programming_guide/events.py`
+
+Playing sounds and music
+------------------------
+
+pyglet makes it easy to play and mix multiple sounds together.
+The following example plays an MP3 file [#mp3]_::
+
+    import pyglet
+
+    music = pyglet.resource.media('music.mp3')
+    music.play()
+
+    pyglet.app.run()
+
+As with the image loading example presented earlier,
+:func:`~pyglet.resource.media` locates the sound file in the application's
+directory (not the working directory).  If you know the actual filesystem path
+(either relative or absolute), use :func:`pyglet.media.load`.
+
+By default, audio is streamed when playing. This works well for longer music
+tracks. Short sounds, such as a gunfire shot used in a game, should instead be
+fully decoded in memory before they are used. This allows them to play more
+immediately and incur less of a CPU performance penalty. It also allows playing
+the same sound repeatedly without reloading it.
+Specify ``streaming=False`` in this case::
+
+    sound = pyglet.resource.media('shot.wav', streaming=False)
+    sound.play()
+
+The `examples/media_player.py` example demonstrates playback of streaming
+audio and video using pyglet.  The `examples/noisy/noisy.py` example
+demonstrates playing many short audio samples simultaneously, as in a game.
+
+.. [#mp3] MP3 and other compressed audio formats require FFmpeg to be
+          installed.
+          Uncompressed WAV files can be played without FFmpeg.
+
+Where to next?
+--------------
+
+The examples above have shown you how to display something on the screen,
+and perform a few basic tasks.  You're probably left with a lot of questions
+about these examples, but don't worry. The remainder of this programming guide
+goes into greater technical detail on many of pyglet's features.  If you're
+an experienced developer, you can probably dive right into the sections that
+interest you.
+
+For new users, it might be daunting to read through everything all at once.
+If you feel overwhelmed, we recommend browsing through the beginnings of each
+chapter, and then having a look at a more in-depth example project.
+You can find an example of a 2D game in the :ref:`programming-guide-game`
+section.
+
+To write advanced 3D applications or achieve optimal performance in your 2D
+applications, you'll need to work with OpenGL directly.  If you only want to
+work with OpenGL primitives, but want something slightly higher-level, have a
+look at the :ref:`guide_graphics` module.
+
+There are numerous examples of pyglet applications in the ``examples/``
+directory of the documentation and source distributions.  If you get
+stuck, or have any questions, join us on the `mailing list`_ or `Discord`_!
+
+.. _mailing list: http://groups.google.com/group/pyglet-users
+.. _Discord: https://discord.gg/QXyegWe
diff --git a/doc/programming_guide/resources.rst b/doc/programming_guide/resources.rst
new file mode 100644
index 0000000..3f04ff0
--- /dev/null
+++ b/doc/programming_guide/resources.rst
@@ -0,0 +1,235 @@
+.. _guide_resources:
+
+Application resources
+=====================
+
+Previous sections in this guide have described how to load images, media and
+text documents using pyglet.  Applications also usually have the need to load
+other data files: for example, level descriptions in a game, internationalised
+strings, and so on.
+
+Programmers are often tempted to load, for example, an image required by their
+application with::
+
+    image = pyglet.image.load('logo.png')
+
+This code assumes ``logo.png`` is in the current working directory.
+Unfortunately the working directory is not necessarily the same as the
+directory containing the application script files.
+
+* Applications started from the command line can start from an arbitrary
+  working directory.
+* Applications bundled into an egg, Mac OS X package or Windows executable
+  may have their resources inside a ZIP file.
+* The application might need to change the working directory in order to
+  work with the user's files.
+
+A common workaround for this is to construct a path relative to the script
+file instead of the working directory::
+
+    import os
+
+    script_dir = os.path.dirname(__file__)
+    path = os.path.join(script_dir, 'logo.png')
+    image = pyglet.image.load(path)
+
+This, besides being tedious to write, still does not work for resources within
+ZIP files, and can be troublesome in projects that span multiple packages.
+
+The :py:mod:`pyglet.resource` module solves this problem elegantly::
+
+    image = pyglet.resource.image('logo.png')
+
+The following sections describe exactly how the resources are located, and how
+the behaviour can be customised.
+
+Loading resources
+-----------------
+
+Use the :py:mod:`pyglet.resource` module when files shipped with the
+application need to be loaded.  For example, instead of writing::
+
+    data_file = open('file.txt')
+
+use::
+
+    data_file = pyglet.resource.file('file.txt')
+
+There are also convenience functions for loading media files for pyglet.  The
+following table shows the equivalent resource functions for the standard file
+functions.
+
+    .. list-table::
+        :header-rows: 1
+
+        * - File function
+          - Resource function
+          - Type
+        * - ``open``
+          - :py:func:`pyglet.resource.file`
+          - File-like object
+        * - :py:func:`pyglet.image.load`
+          - :py:func:`pyglet.resource.image`
+          - :py:class:`~pyglet.image.Texture` or :py:class:`~pyglet.image.TextureRegion`
+        * - :py:func:`pyglet.image.load`
+          - :py:func:`pyglet.resource.texture`
+          - :py:class:`~pyglet.image.Texture`
+        * - :py:func:`pyglet.image.load_animation`
+          - :py:func:`pyglet.resource.animation`
+          - :py:class:`~pyglet.image.Animation`
+        * - :py:func:`pyglet.media.load`
+          - :py:func:`pyglet.resource.media`
+          - :py:class:`~pyglet.media.Source`
+        * - | :py:func:`pyglet.text.load`
+            | mimetype = ``text/plain``
+          - :py:func:`pyglet.resource.text`
+          - :py:class:`~pyglet.text.document.UnformattedDocument`
+        * - | :py:func:`pyglet.text.load`
+            | mimetype = ``text/html``
+          - :py:func:`pyglet.resource.html`
+          - :py:class:`~pyglet.text.document.FormattedDocument`
+        * - | :py:func:`pyglet.text.load`
+            | mimetype = ``text/vnd.pyglet-attributed``
+          - :py:func:`pyglet.resource.attributed`
+          - :py:class:`~pyglet.text.document.FormattedDocument`
+        * - :py:func:`pyglet.font.add_file`
+          - :py:func:`pyglet.resource.add_font`
+          - ``None``
+
+:py:func:`pyglet.resource.texture` is for loading stand-alone textures.
+This can be useful when using the texture for a 3D model, or generally
+working with OpenGL directly.
+
+:py:func:`pyglet.resource.image` is optimised for loading sprite-like
+images that can have their texture coordinates adjusted.
+The resource module attempts to pack small images into larger texture atlases
+(explained in :ref:`guide_texture-bins-and-atlases`) for efficient rendering
+(which is why the return type of this function can be
+:py:class:`~pyglet.image.TextureRegion`).
+It is also advisable to use the texture atlas classes directly if you wish
+to have different achor points on multiple copies of the same image.
+This is because when loading an image more than once, you will actually get
+the **same** object back. You can still use the resource module for getting
+the image location, and described in the next section.
+
+
+Resource locations
+^^^^^^^^^^^^^^^^^^
+
+Some resource files reference other files by name.  For example, an HTML
+document can contain ``<img src="image.png" />`` elements.  In this case your
+application needs to locate ``image.png`` relative to the original HTML file.
+
+Use :py:func:`pyglet.resource.location` to get a
+:py:class:`~pyglet.resource.Location` object describing the location of an
+application resource.  This location might be a file system
+directory or a directory within a ZIP file.
+The :py:class:`~pyglet.resource.Location` object can directly open files by
+name, so your application does not need to distinguish between these cases.
+
+In the following example, a ``thumbnails.txt`` file is assumed to contain a
+list of image filenames (one per line), which are then loaded assuming the
+image files are located in the same directory as the ``thumbnails.txt`` file::
+
+    thumbnails_file = pyglet.resource.file('thumbnails.txt', 'rt')
+    thumbnails_location = pyglet.resource.location('thumbnails.txt')
+
+    for line in thumbnails_file:
+        filename = line.strip()
+        image_file = thumbnails_location.open(filename)
+        image = pyglet.image.load(filename, file=image_file)
+        # Do something with `image`...
+
+This code correctly ignores other images with the same filename that might
+appear elsewhere on the resource path.
+
+Specifying the resource path
+----------------------------
+
+By default, only the script home directory is searched (the directory
+containing the ``__main__`` module).
+You can set :py:attr:`pyglet.resource.path` to a list of locations to
+search in order.  This list is indexed, so after modifying it you will
+need to call :py:func:`pyglet.resource.reindex`.
+
+Each item in the path list is either a path relative to the script home, or
+the name of a Python module preceded with an "at" symbol (``@``).  For example,
+if you would like to package all your resources in a ``res`` directory::
+
+    pyglet.resource.path = ['res']
+    pyglet.resource.reindex()
+
+Items on the path are not searched recursively, so if your resource directory
+itself has subdirectories, these need to be specified explicitly::
+
+    pyglet.resource.path = ['res', 'res/images', 'res/sounds', 'res/fonts']
+    pyglet.resource.reindex()
+
+The entries in the resource path always use forward slash characters as path
+separators even when the operating systems using a different character.
+
+Specifying module names makes it easy to group code with its resources.  The
+following example uses the directory containing the hypothetical
+``gui.skins.default`` for resources::
+
+    pyglet.resource.path = ['@gui.skins.default', '.']
+    pyglet.resource.reindex()
+
+Multiple loaders
+----------------
+
+A :py:class:`~pyglet.resource.Loader` encapsulates a complete resource path
+and cache.  This lets your application cleanly separate resource loading of
+different modules.
+Loaders are constructed for a given search path, andnexposes the same methods
+as the global :py:mod:`pyglet.resource` module functions.
+
+For example, if a module needs to load its own graphics but does not want to
+interfere with the rest of the application's resource loading, it would create
+its own :py:class:`~pyglet.resource.Loader` with a local search path::
+
+    loader = pyglet.resource.Loader(['@' + __name__])
+    image = loader.image('logo.png')
+
+This is particularly suitable for "plugin" modules.
+
+You can also use a :py:class:`~pyglet.resource.Loader` instance to load a set
+of resources relative to some user-specified document directory.
+The following example creates a loader for a directory specified on the
+command line::
+
+    import sys
+    home = sys.argv[1]
+    loader = pyglet.resource.Loader(script_home=[home])
+
+This is the only way that absolute directories and resources not bundled with
+an application should be used with :py:mod:`pyglet.resource`.
+
+Saving user preferences
+-----------------------
+
+Because Python applications can be distributed in several ways, including
+within ZIP files, it is usually not feasible to save user preferences, high
+score lists, and so on within the application directory (or worse, the working
+directory).
+
+The :py:func:`pyglet.resource.get_settings_path` function returns a directory
+suitable for writing arbitrary user-centric data. The directory used follows
+the operating system's convention:
+
+* ``~/.config/ApplicationName/`` on Linux (depends on `XDG_CONFIG_HOME`
+  environment variable).
+* ``$HOME\Application Settings\ApplicationName`` on Windows
+* ``~/Library/Application Support/ApplicationName`` on Mac OS X
+
+The returned directory name is not guaranteed to exist -- it is the
+application's responsibility to create it.  The following example opens a high
+score list file for a game called "SuperGame" into the settings directory::
+
+    import os
+
+    dir = pyglet.resource.get_settings_path('SuperGame')
+    if not os.path.exists(dir):
+        os.makedirs(dir)
+    filename = os.path.join(dir, 'highscores.txt')
+    file = open(filename, 'wt')
diff --git a/doc/programming_guide/shapes.rst b/doc/programming_guide/shapes.rst
new file mode 100644
index 0000000..ab802af
--- /dev/null
+++ b/doc/programming_guide/shapes.rst
@@ -0,0 +1,62 @@
+Shapes
+======
+
+.. _guide_shapes:
+
+
+The :py:mod:`~pyglet.shapes` module is a simplified option for creating
+and manipulating colored shapes. This includes rectangles, circles, and
+lines. Shapes can be resized, positioned, and rotated where applicable,
+and their color and opacity can be changed. All shapes are implemented
+using OpenGL primitives, so they can be drawn efficiently with :ref:`guide_batched-rendering`.
+In the following examples `Batch` will be ommitted for brevity, but in
+general you always want to use Batched rendering for performance.
+
+For drawing more complex shapes, see the :ref:`guide_graphics` module.
+
+
+Creating a Shape
+----------------
+
+Various shapes can be constructed with a specific position, size, and color::
+
+    circle = shapes.Circle(x=100, y=150, radius=100, color=(50, 225, 30))
+    square = shapes.Rectangle(x=200, y=200, width=200, height=200, color=(55, 55, 255))
+
+You can also change the color, or set the opacity after creation. The opacity
+can can be set on a scale of 0-255, for various levels of transparency::
+
+    circle.opacity = 120
+
+The size of Shapes can also be adjusted after creation::
+
+    square.width = 200
+    circle.radius = 99
+
+
+Anchor Points
+^^^^^^^^^^^^^
+
+Similar to images in pyglet, the "anchor point" of a Shape can be set.
+This relates to the center of the shape on the x and y axis. For Circles,
+the default anchor point is the center of the circle. For Rectangles,
+it is the bottom left corner. Depending on how you need to position your
+Shapes, this can be changed. For Rectangles this is especially useful if
+you will rotate it, since Shapes will rotate around the anchor point. In
+this example, a Rectangle is created, and the anchor point is then set to
+the center::
+
+    rectangle = shapes.Rectangle(x=400, y=400, width=100, height=50)
+    rectangle.anchor_x = 50
+    rectangle.anchor_y = 25
+    # or, set at the same time:
+    rectangle.anchor_position = 50, 25
+
+    # The rectangle is then rotated around it's anchor point:
+    rectangle.rotation = 45
+
+If you plan to create a large number of shapes, you can optionally set the
+default anchor points::
+
+    shapes.Rectangle._anchor_x = 100
+    shapes.Rectangle._anchor_y = 50
diff --git a/doc/programming_guide/text.rst b/doc/programming_guide/text.rst
new file mode 100644
index 0000000..9c0b01a
--- /dev/null
+++ b/doc/programming_guide/text.rst
@@ -0,0 +1,792 @@
+Displaying text
+===============
+
+pyglet provides the :py:mod:`~pyglet.font` module for efficiently rendering
+high-quality antialiased Unicode glyphs. pyglet can use any installed font
+on the operating system, or you can provide your own font with your
+application.
+
+Please note that not all font formats are supported,
+see :ref:`guide_supported-font-formats`
+
+Text rendering is performed with the :py:mod:`~pyglet.text` module, which
+can display word-wrapped formatted text.  There is also support for
+interactive editing of text on-screen with a caret.
+
+Simple text rendering
+---------------------
+
+The following complete example creates a window that displays
+"Hello, World"  centered vertically and horizontally::
+
+    window = pyglet.window.Window()
+    label = pyglet.text.Label('Hello, world',
+                              font_name='Times New Roman',
+                              font_size=36,
+                              x=window.width//2, y=window.height//2,
+                              anchor_x='center', anchor_y='center')
+
+    @window.event
+    def on_draw():
+        window.clear()
+        label.draw()
+
+    pyglet.app.run()
+
+The example demonstrates the most common uses of text rendering:
+
+* The font name and size are specified directly in the constructor.
+  Additional parameters exist for setting the bold and italic styles and the
+  color of the text.
+* The position of the text is given by the ``x`` and ``y`` coordinates.  The
+  meaning of these coordinates is given by the ``anchor_x`` and ``anchor_y``
+  parameters.
+* The actual drawing of the text to the screen is done with the
+  :py:meth:`pyglet.text.Label.draw` method.  Labels can also be added to a
+  graphics batch; see :ref:`guide_batched-rendering` for details.
+
+The :py:func:`~pyglet.text.HTMLLabel` class is used similarly, but accepts
+an HTML formatted string instead of parameters describing the style.
+This allows the label to display text with mixed style::
+
+    label = pyglet.text.HTMLLabel(
+        '<font face="Times New Roman" size="4">Hello, <i>world</i></font>',
+        x=window.width//2, y=window.height//2,
+        anchor_x='center', anchor_y='center')
+
+See :ref:`guide_formatted-text` for details on the subset of HTML that is
+supported.
+
+The document/layout model
+-------------------------
+
+The :py:func:`~pyglet.text.Label` class demonstrated above presents a
+simplified interface to pyglet's complete text rendering capabilities.
+The underlying :py:func:`~pyglet.text.layout.TextLayout` and
+:py:class:`~pyglet.text.document.AbstractDocument` classes provide a
+"model/view" interface to all of pyglet's text features.
+
+    .. image:: img/text_classes.png
+
+Documents
+^^^^^^^^^
+
+A `document` is the "model" part of the architecture, and describes the
+content and style of the text to be displayed.  There are two concrete
+document classes: :py:class:`~pyglet.text.document.UnformattedDocument`
+and :py:class:`~pyglet.text.document.FormattedDocument`.
+:py:class:`~pyglet.text.document.UnformattedDocument` models a document
+containing text in just one style, whereas
+:py:class:`~pyglet.text.document.FormattedDocument` allows the style to
+change within the text.
+
+An empty, unstyled document can be created by constructing either of the
+classes directly.  Usually you will want to initialise the document with some
+text, however. The :py:func:`~pyglet.text.decode_text`,
+:py:func:`~pyglet.text.decode_attributed` and
+:py:func:`~pyglet.text.decode_html` functions return a document given a
+source string. For :py:func:`~pyglet.text.decode_text`,
+this is simply a plain text string, and the return value is an
+:py:class:`~pyglet.text.document.UnformattedDocument`::
+
+    document = pyglet.text.decode_text('Hello, world.')
+
+:py:func:`~pyglet.text.decode_attributed` and
+:py:func:`~pyglet.text.decode_html` are described in detail in the next
+section.
+
+The text of a document can be modified directly as a property on the object::
+
+    document.text = 'Goodbye, cruel world.'
+
+However, if small changes are being made to the document it can be more
+efficient (when coupled with an appropriate layout; see below) to use the
+:py:func:`~pyglet.text.document.AbstractDocument.delete_text` and
+:py:func:`~pyglet.text.document.AbstractDocument.insert_text` methods instead.
+
+Layouts
+^^^^^^^
+
+The actual layout and rendering of a document is performed by the
+:py:func:`~pyglet.text.layout.TextLayout` classes.
+This split exists to reduce the complexity of the code, and to allow
+a single document to be displayed in multiple layouts simultaneously (in other
+words, many layouts can display one document).
+
+Each of the :py:func:`~pyglet.text.layout.TextLayout` classes perform layout
+in the same way, but represent a trade-off in efficiency of update against
+efficiency of drawing and memory usage.
+
+The base :py:func:`~pyglet.text.layout.TextLayout` class uses little memory,
+and shares its graphics group with other
+:py:func:`~pyglet.text.layout.TextLayout` instances in the same batch
+(see :ref:`guide_batched-rendering`). When the text or style of the document
+is modified, or the layout constraints change (for example, the width of the
+layout changes), the entire text layout is recalculated.
+This is a potentially expensive operation, especially for long documents.
+This makes :py:func:`~pyglet.text.layout.TextLayout` suitable
+for relatively short or unchanging documents.
+
+:py:class:`~pyglet.text.layout.ScrollableTextLayout` is a small extension to
+:py:func:`~pyglet.text.layout.TextLayout` that clips the
+text to a specified view rectangle, and allows text to be scrolled within that
+rectangle without performing the layout calculuation again.  Because of this
+clipping rectangle the graphics group cannot be shared with other text
+layouts, so for ideal performance
+:py:class:`~pyglet.text.layout.ScrollableTextLayout` should be used only
+if this behaviour is required.
+
+:py:class:`~pyglet.text.layout.IncrementalTextLayout` uses a more sophisticated
+layout algorithm that performs less work for small changes to documents.
+For example, if a document is being edited by the user, only the immediately
+affected lines of text are recalculated when a character is typed or deleted.
+:py:class:`~pyglet.text.layout.IncrementalTextLayout`
+also performs view rectangle culling, reducing the amount of layout and
+rendering required when the document is larger than the view.
+:py:class:`~pyglet.text.layout.IncrementalTextLayout` should be used for
+large documents or documents that change rapidly.
+
+All the layout classes can be constructed given a document and display
+dimensions::
+
+    layout = pyglet.text.layout.TextLayout(document, width, height)
+
+Additional arguments to the constructor allow the specification of a graphics
+batch and group (recommended if many layouts are to be rendered), and the
+optional `multiline` and `wrap_lines` flags.
+
+`multiline`
+  To honor newlines in the document you will need to set this to ``True``. If
+  you do not then newlines will be rendered as plain spaces.
+
+`wrap_lines`
+  If you expect that your document lines will be wider than the display width
+  then pyglet can automatically wrap them to fit the width by setting this
+  option to ``True``.
+
+Like labels, layouts are positioned through their `x`, `y`,
+`anchor_x` and `anchor_y` properties.
+Note that unlike :py:class:`~pyglet.image.AbstractImage`, the `anchor`
+properties accept a string such as ``"bottom"`` or ``"center"`` instead of a
+numeric displacement.
+
+.. _guide_formatted-text:
+
+Formatted text
+--------------
+
+The :py:class:`~pyglet.text.document.FormattedDocument` class maintains
+style information for individual characters in the text, rather than a
+single style for the whole document.
+Styles can be accessed and modified by name, for example::
+
+    # Get the font name used at character index 0
+    font_name = document.get_style('font_name', 0)
+
+    # Set the font name and size for the first 5 characters
+    document.set_style(0, 5, dict(font_name='Arial', font_size=12))
+
+Internally, character styles are run-length encoded over the document text; so
+longer documents with few style changes do not use excessive memory.
+
+From the document's point of view, there are no predefined style names: it
+simply maps names and character ranges to arbitrary Python values.
+It is the :py:class:`~pyglet.text.layout.TextLayout` classes that interpret
+this style information; for example, by selecting a different font based on the
+``font_name`` style.  Unrecognised style names are ignored by the layout
+-- you can use this knowledge to store additional data alongside the
+document text (for example, a URL behind a hyperlink).
+
+Character styles
+^^^^^^^^^^^^^^^^
+
+The following character styles are recognised by all
+:py:func:`~pyglet.text.layout.TextLayout` classes.
+
+Where an attribute is marked "as a distance" the value is assumed to be
+in pixels if given as an int or float, otherwise a string of the form
+``"0u"`` is required, where ``0`` is the distance and ``u`` is the unit; one
+of ``"px"`` (pixels), ``"pt"`` (points), ``"pc"`` (picas), ``"cm"``
+(centimeters), ``"mm"`` (millimeters) or ``"in"`` (inches).  For example,
+``"14pt"`` is the distance covering 14 points, which at the default DPI of 96
+is 18 pixels.
+
+``font_name``
+    Font family name, as given to :py:func:`pyglet.font.load`.
+``font_size``
+    Font size, in points.
+``bold``
+    Boolean.
+``italic``
+    Boolean.
+``underline``
+    4-tuple of ints in range (0, 255) giving RGBA underline color, or None
+    (default) for no underline.
+``kerning``
+    Additional space to insert between glyphs, as a distance.  Defaults to 0.
+``baseline``
+    Offset of glyph baseline from line baseline, as a distance.  Positive
+    values give a superscript, negative values give a subscript.  Defaults to
+    0.
+``color``
+    4-tuple of ints in range (0, 255) giving RGBA text color
+``background_color``
+    4-tuple of ints in range (0, 255) giving RGBA text background color; or
+    ``None`` for no background fill.
+
+Paragraph styles
+^^^^^^^^^^^^^^^^
+
+Although :py:class:`~pyglet.text.document.FormattedDocument` does not
+distinguish between character- and paragraph-level styles,
+:py:func:`~pyglet.text.layout.TextLayout` interprets the following styles
+only at the paragraph level. You should take care to set these styles for
+complete paragraphs only, for example, by using
+:py:meth:`~pyglet.text.document.AbstractDocument.set_paragraph_style`.
+
+These styles are ignored for layouts without the ``multiline`` flag set.
+
+``align``
+    ``"left"`` (default), ``"center"`` or ``"right"``.
+``indent``
+    Additional horizontal space to insert before the first glyph of the
+    first line of a paragraph, as a distance.
+``leading``
+    Additional space to insert between consecutive lines within a paragraph,
+    as a distance.  Defaults to 0.
+``line_spacing``
+    Distance between consecutive baselines in a paragraph, as a distance.
+    Defaults to ``None``, which automatically calculates the tightest line
+    spacing for each line based on the maximum font ascent and descent.
+``margin_left``
+    Left paragraph margin, as a distance.
+``margin_right``
+    Right paragraph margin, as a distance.
+``margin_top``
+    Margin above paragraph, as a distance.
+``margin_bottom``
+    Margin below paragraph, as a distance.  Adjacent margins do not collapse.
+``tab_stops``
+    List of horizontal tab stops, as distances, measured from the left edge of
+    the text layout.  Defaults to the empty list.  When the tab stops
+    are exhausted, they implicitly continue at 50 pixel intervals.
+``wrap``
+    Boolean.  If True (the default), text wraps within the width of the layout.
+
+For the purposes of these attributes, paragraphs are split by the newline
+character (U+0010) or the paragraph break character (U+2029).  Line breaks
+within a paragraph can be forced with character U+2028.
+
+Tabs
+....
+
+A tab character in pyglet text is interpreted as 'move to the next tab stop'.
+Tab stops are specified in pixels, not in some font unit; by default
+there is a tab stop every 50 pixels and because of that a tab can look too
+small for big fonts or too big for small fonts.
+
+Additionally, when rendering text with tabs using a `monospace` font,
+character boxes may not align vertically.
+
+To avoid these visualization issues the simpler solution is to convert
+the tabs to spaces before sending a string to a pyglet text-related class.
+
+Attributed text
+^^^^^^^^^^^^^^^
+
+pyglet provides two formats for decoding formatted documents from plain text.
+These are useful for loading preprepared documents such as help screens.  At
+this time there is no facility for saving (encoding) formatted documents.
+
+The *attributed text* format is an encoding specific to pyglet that can
+exactly describe any :py:class:`~pyglet.text.document.FormattedDocument`.
+You must use this encoding to access all of the features of pyglet text layout.
+For a more accessible, yet less featureful encoding,
+see the `HTML` encoding, described below.
+
+The following example shows a simple attributed text encoded document:
+
+.. rst-class:: plain
+
+  ::
+
+    Chapter 1
+
+    My father's family name being Pirrip, and my Christian name Philip,
+    my infant tongue could make of both names nothing longer or more
+    explicit than Pip.  So, I called myself Pip, and came to be called
+    Pip.
+
+    I give Pirrip as my father's family name, on the authority of his
+    tombstone and my sister - Mrs. Joe Gargery, who married the
+    blacksmith.  As I never saw my father or my mother, and never saw
+    any likeness of either of them (for their days were long before the
+    days of photographs), my first fancies regarding what they were
+    like, were unreasonably derived from their tombstones.
+
+Newlines are ignored, unless two are made in succession, indicating a
+paragraph break.  Line breaks can be forced with the ``\\`` sequence:
+
+.. rst-class:: plain
+
+  ::
+
+    This is the way the world ends \\
+    This is the way the world ends \\
+    This is the way the world ends \\
+    Not with a bang but a whimper.
+
+Line breaks are also forced when the text is indented with one or more spaces
+or tabs, which is useful for typesetting code:
+
+.. rst-class:: plain
+
+  ::
+
+    The following paragraph has hard line breaks for every line of code:
+
+        import pyglet
+
+        window = pyglet.window.Window()
+        pyglet.app.run()
+
+Text can be styled using a attribute tag:
+
+.. rst-class:: plain
+
+  ::
+
+    This sentence makes a {bold True}bold{bold False} statement.
+
+The attribute tag consists of the attribute name (in this example, ``bold``)
+followed by a Python bool, int, float, string, tuple or list.
+
+Unlike most structured documents such as HTML, attributed text has no concept
+of the "end" of a style; styles merely change within the document.
+This corresponds exactly to the representation used by
+:py:class:`~pyglet.text.document.FormattedDocument` internally.
+
+Some more examples follow:
+
+.. rst-class:: plain
+
+  ::
+
+    {font_name 'Times New Roman'}{font_size 28}Hello{font_size 12},
+    {color (255, 0, 0, 255)}world{color (0, 0, 0, 255)}!
+
+(This example uses 28pt Times New Roman for the word "Hello", and 12pt
+red text for the word "world").
+
+Paragraph styles can be set by prefixing the style name with a period (.).
+This ensures the style range exactly encompasses the paragraph:
+
+.. rst-class:: plain
+
+  ::
+
+    {.margin_left "12px"}This is a block quote, as the margin is inset.
+
+    {.margin_left "24px"}This paragraph is inset yet again.
+
+Attributed text can be loaded as a Unicode string.  In addition, any character
+can be inserted given its Unicode code point in numeric form, either in
+decimal:
+
+.. rst-class:: plain
+
+  ::
+
+    This text is Copyright {#169}.
+
+or hexadecimal:
+
+.. rst-class:: plain
+
+  ::
+
+    This text is Copyright {#xa9}.
+
+The characters ``{`` and ``}`` can be escaped by duplicating them:
+
+.. rst-class:: plain
+
+  ::
+
+    Attributed text uses many "{{" and "}}" characters.
+
+Use the ``decode_attributed`` function to decode attributed text into a
+:py:class:`~pyglet.text.document.FormattedDocument`::
+
+    document = pyglet.text.decode_attributed('Hello, {bold True}world')
+
+HTML
+^^^^
+
+While attributed text gives access to all of the features of
+:py:class:`~pyglet.text.document.FormattedDocument` and
+:py:func:`~pyglet.text.layout.TextLayout`, it is quite verbose and difficult
+produce text in.  For convenience, pyglet provides an HTML 4.01 decoder that
+can translate a small, commonly used subset of HTML into a
+:py:class:`~pyglet.text.document.FormattedDocument`.
+
+Note that the decoder does not preserve the structure of the HTML document --
+all notion of element hierarchy is lost in the translation, and only the
+visible style changes are preserved.
+
+The following example uses :py:func:`~pyglet.text.decode_html` to create a
+:py:class:`~pyglet.text.document.FormattedDocument` from a string of HTML::
+
+    document = pyglet.text.decode_html('Hello, <b>world</b>')
+
+The following elements are supported:
+
+.. rst-class:: plain
+
+  ::
+
+    B BLOCKQUOTE BR CENTER CODE DD DIR DL EM FONT H1 H2 H3 H4 H5 H6 I IMG KBD
+    LI MENU OL P PRE Q SAMP STRONG SUB SUP TT U UL VAR
+
+The ``style`` attribute is not supported, so font sizes must be given as HTML
+logical sizes in the range 1 to 7, rather than as point sizes.  The
+corresponding font sizes, and some other stylesheet parameters, can be
+modified by subclassing `HTMLDecoder`.
+
+Custom elements
+---------------
+
+Graphics and other visual elements can be inserted inline into a document
+using :py:meth:`~pyglet.text.document.AbstractDocument.insert_element`.
+For example, inline elements are used to render HTML images included with
+the ``IMG`` tag.  There is currently no support for floating or
+absolutely-positioned elements.
+
+Elements must subclass :py:class:`~pyglet.text.document.InlineElement`
+and override the `place` and `remove` methods.  These methods are called by
+:py:func:`~pyglet.text.layout.TextLayout` when the element becomes
+or ceases to be visible.  For :py:func:`~pyglet.text.layout.TextLayout`
+and :py:class:`~pyglet.text.layout.ScrollableTextLayout`,
+this is when the element is added or removed from the document;
+but for :py:class:`~pyglet.text.layout.IncrementalTextLayout` the methods
+are also called as the element scrolls in and out of the viewport.
+
+The constructor of :py:class:`~pyglet.text.document.InlineElement`
+gives the width and height (separated into the ascent above the baseline,
+and descent below the baseline) of the element.
+
+Typically an :py:class:`~pyglet.text.document.InlineElement` subclass will
+add graphics primitives to the layout's graphics batch; though applications
+may choose to simply record the position of the element and render it
+separately.
+
+The position of the element in the document text is marked with a NUL
+character (U+0000) placeholder.  This has the effect that inserting an element
+into a document increases the length of the document text by one.  Elements
+can also be styled as if they were ordinary character text, though the layout
+ignores any such style attributes.
+
+User-editable text
+------------------
+
+While pyglet does not come with any complete GUI widgets for applications to
+use, it does implement many of the features required to implement interactive
+text editing.  These can be used as a basis for a more complete GUI system, or
+to present a simple text entry field, as demonstrated in the
+``examples/text_input.py`` example.
+
+:py:class:`~pyglet.text.layout.IncrementalTextLayout` should always be used for
+text that can be edited by the user.
+This class maintains information about the placement of glyphs on screen,
+and so can map window coordinates to a document position and vice-versa.
+These methods are
+:py:meth:`~pyglet.text.layout.IncrementalTextLayout.get_position_from_point`,
+:py:meth:`~pyglet.text.layout.IncrementalTextLayout.get_point_from_position`,
+:py:meth:`~pyglet.text.layout.IncrementalTextLayout.get_line_from_point`,
+:py:meth:`~pyglet.text.layout.IncrementalTextLayout.get_point_from_line`,
+:py:meth:`~pyglet.text.layout.IncrementalTextLayout.get_line_from_position`,
+:py:meth:`~pyglet.text.layout.IncrementalTextLayout.get_position_from_line`,
+:py:meth:`~pyglet.text.layout.IncrementalTextLayout.get_position_on_line`
+and
+:py:meth:`~pyglet.text.layout.IncrementalTextLayout.get_line_count`.
+
+The viewable rectangle of the document can be adjusted using a document
+position instead of a scrollbar using the
+:py:meth:`~pyglet.text.layout.IncrementalTextLayout.ensure_line_visible` and
+:py:meth:`~pyglet.text.layout.IncrementalTextLayout.ensure_x_visible` methods.
+
+:py:class:`~pyglet.text.layout.IncrementalTextLayout` can display a current
+text selection by temporarily overriding the foreground and background colour
+of the selected text. The
+:py:attr:`~pyglet.text.layout.IncrementalTextLayout.selection_start` and
+:py:attr:`~pyglet.text.layout.IncrementalTextLayout.selection_end` properties
+give the range of the selection, and
+:py:attr:`~pyglet.text.layout.IncrementalTextLayout.selection_color` and
+:py:attr:`~pyglet.text.layout.IncrementalTextLayout.selection_background_color`
+the colors to use (defaulting to white on blue).
+
+The :py:class:`~pyglet.text.caret.Caret` class implements an insertion caret
+(cursor) for :py:class:`~pyglet.text.layout.IncrementalTextLayout`.
+This includes displaying the blinking caret at the correct location,
+and handling keyboard, text and mouse events.
+The behaviour in response to the events is very similar to the system GUIs
+on Windows, Mac OS X and GTK.  Using :py:class:`~pyglet.text.caret.Caret`
+frees you from using the :py:class:`~pyglet.text.layout.IncrementalTextLayout`
+methods described above directly.
+
+The following example creates a document, a layout and a caret and attaches
+the caret to the window to listen for events::
+
+    import pyglet
+
+    window = pyglet.window.Window()
+    document = pyglet.text.document.FormattedDocument()
+    layout = pyglet.text.layout.IncrementalTextLayout(document, width, height)
+    caret = pyglet.text.caret.Caret(layout)
+    window.push_handlers(caret)
+
+When the layout is drawn, the caret will also be drawn, so this example is
+nearly complete enough to display the user input.  However, it is suitable for
+use when only one editable text layout is to be in the window.  If multiple
+text widgets are to be shown, some mechanism is needed to dispatch events to
+the widget that has keyboard focus.  An example of how to do this is given in
+the `examples/text_input.py` example program.
+
+Loading system fonts
+--------------------
+
+The layout classes automatically load fonts as required.  You can also
+explicitly load fonts to implement your own layout algorithms.
+
+To load a font you must know its family name.  This is the name displayed in
+the font dialog of any application.  For example, all operating systems
+include the *Times New Roman* font.  You must also specify the font size to
+load, in points::
+
+    # Load "Times New Roman" at 16pt
+    times = pyglet.font.load('Times New Roman', 16)
+
+Bold and italic variants of the font can specified with keyword parameters::
+
+    times_bold = pyglet.font.load('Times New Roman', 16, bold=True)
+    times_italic = pyglet.font.load('Times New Roman', 16, italic=True)
+    times_bold_italic = pyglet.font.load('Times New Roman', 16,
+                                         bold=True, italic=True)
+
+For maximum compatibility on all platforms, you can specify a list of font
+names to load, in order of preference.  For example, many users will have
+installed the Microsoft Web Fonts pack, which includes `Verdana`, but this
+cannot be guaranteed, so you might specify `Arial` or `Helvetica` as
+suitable alternatives::
+
+    sans_serif = pyglet.font.load(('Verdana', 'Helvetica', 'Arial'), 16)
+
+Also you can check for the availability of a font using
+:py:func:`pyglet.font.have_font`::
+
+    # Will return True
+    pyglet.font.have_font('Times New Roman')
+
+    # Will return False
+    pyglet.font.have_font('missing-font-name')
+
+If you do not particularly care which font is used, and just need to display
+some readable text, you can specify `None` as the family name, which will load
+a default sans-serif font (Helvetica on Mac OS X, Arial on Windows XP)::
+
+    sans_serif = pyglet.font.load(None, 16)
+
+Font sizes
+----------
+
+When loading a font you must specify the font size it is to be rendered at, in
+points.  Points are a somewhat historical but conventional unit used in both
+display and print media.  There are various conflicting definitions for the
+actual length of a point, but pyglet uses the PostScript definition: 1 point =
+1/72 inches.
+
+Font resolution
+^^^^^^^^^^^^^^^
+
+The actual rendered size of the font on screen depends on the display
+resolution. pyglet uses a default DPI of 96 on all operating systems.  Most
+Mac OS X applications use a DPI of 72, so the font sizes will not match up on
+that operating system.  However, application developers can be assured that
+font sizes remain consistent in pyglet across platforms.
+
+The DPI can be specified directly in the :py:func:`pyglet.font.load`
+function, and as an argument to the :py:func:`~pyglet.text.layout.TextLayout`
+constructor.
+
+Determining font size
+^^^^^^^^^^^^^^^^^^^^^
+
+Once a font is loaded at a particular size, you can query its pixel size with
+the attributes::
+
+    Font.ascent
+    Font.descent
+
+These measurements are shown in the diagram below.
+
+.. figure:: img/font_metrics.png
+
+    Font metrics.  Note that the descent is usually negative as it descends
+    below the baseline.
+
+You can calculate the distance between successive lines of text as::
+
+    ascent - descent + leading
+
+where `leading` is the number of pixels to insert between each line of text.
+
+Loading custom fonts
+--------------------
+
+You can supply a font with your application if it's not commonly installed on
+the target platform.  You should ensure you have a license to distribute the
+font -- the terms are often specified within the font file itself, and can be
+viewed with your operating system's font viewer.
+
+Loading a custom font must be performed in two steps:
+
+1. Let pyglet know about the additional font or font files.
+2. Load the font by its family name.
+
+For example, let's say you have the *Action Man* font in a file called
+``action_man.ttf``.  The following code will load an instance of that font::
+
+    pyglet.font.add_file('action_man.ttf')
+    action_man = pyglet.font.load('Action Man')
+
+Similarly, once the font file has been added, the font name can be specified
+as a style on a label or layout::
+
+    label = pyglet.text.Label('Hello', font_name='Action Man')
+
+Fonts are often distributed in separate files for each variant.  *Action Man
+Bold* would probably be distributed as a separate file called
+``action_man_bold.ttf``; you need to let pyglet know about this as well::
+
+    font.add_file('action_man_bold.ttf')
+    action_man_bold = font.load('Action Man', bold=True)
+
+Note that even when you know the filename of the font you want to load, you
+must specify the font's family name to :py:func:`pyglet.font.load`.
+
+You need not have the file on disk to add it to pyglet; you can specify any
+file-like object supporting the `read` method.  This can be useful for
+extracting fonts from a resource archive or over a network.
+
+If the custom font is distributed with your application, consider using the
+:ref:`guide_resources`.
+
+.. _guide_supported-font-formats:
+
+Supported font formats
+^^^^^^^^^^^^^^^^^^^^^^
+
+pyglet can load any font file that the operating system natively supports,
+but not all formats all fully supported.
+
+The list of supported formats is shown in the table below.
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Font Format
+          - Windows
+          - Mac OS X
+          - Linux (FreeType)
+        * - TrueType (.ttf)
+          - X
+          - X
+          - X
+        * - PostScript Type 1 (.pfm, .pfb)
+          - X
+          - X
+          - X
+        * - Windows Bitmap (.fnt)
+          - X
+          -
+          - X
+        * - Mac OS X Data Fork Font (.dfont)
+          -
+          - X
+          -
+        * - OpenType (.otf) [#opentype]_
+          -
+          - X
+          -
+        * - X11 font formats PCF, BDF, SFONT
+          -
+          -
+          - X
+        * - Bitstream PFR (.pfr)
+          -
+          -
+          - X
+
+.. [#opentype] All OpenType fonts are backward compatible with TrueType, so
+               while the advanced OpenType features can only be rendered with
+               Mac OS X, the files can be used on any platform.  pyglet
+               does not currently make use of the additional kerning and
+               ligature information within OpenType fonts.
+               In Windows a few will use the variant DEVICE_FONTTYPE and may
+               render bad, by example inconsolata.otf, from
+               http://levien.com/type/myfonts/inconsolata.html
+
+Some of the fonts found in internet may miss information for some operating
+systems, others may have been written with work in progress tools not fully
+compliant with standards. Using the font with text editors or fonts viewers
+can help to determine if the font is broken.
+
+OpenGL font considerations
+--------------------------
+
+Text in pyglet is drawn using textured quads.  Each font maintains a set of
+one or more textures, into which glyphs are uploaded as they are needed.  For
+most applications this detail is transparent and unimportant, however some of
+the details of these glyph textures are described below for advanced users.
+
+Context affinity
+^^^^^^^^^^^^^^^^
+
+When a font is loaded, it immediately creates a texture in the current
+context's object space.  Subsequent textures may need to be created if there
+is not enough room on the first texture for all the glyphs.  This is done when
+the glyph is first requested.
+
+pyglet always assumes that the object space that was active when the font was
+loaded is the active one when any texture operations are performed.  Normally
+this assumption is valid, as pyglet shares object spaces between all contexts
+by default.  There are a few situations in which this will not be the case,
+though:
+
+* When explicitly setting the context share during context creation.
+* When multiple display devices are being used which cannot support a shared
+  context object space.
+
+In any of these cases, you will need to reload the font for each object space
+that it's needed in.  pyglet keeps a cache of fonts, but does so
+per-object-space, so it knows when it can reuse an existing font instance or
+if it needs to load it and create new textures.  You will also need to ensure
+that an appropriate context is active when any glyphs may need to be added.
+
+Blend state
+^^^^^^^^^^^
+
+The glyph textures have an internal format of ``GL_ALPHA``, which provides
+a simple way to recolour and blend antialiased text by changing the
+vertex colors.  pyglet makes very few assumptions about the OpenGL state, and
+will not alter it besides changing the currently bound texture.
+
+The following blend state is used for drawing font glyphs::
+
+    from pyglet.gl import *
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+    glEnable(GL_BLEND)
+
+All glyph textures use the ``GL_TEXTURE_2D`` target, so you should ensure that
+a higher priority target such as ``GL_TEXTURE_3D`` is not enabled before
+trying to render text.
diff --git a/doc/programming_guide/time.rst b/doc/programming_guide/time.rst
new file mode 100644
index 0000000..12e389e
--- /dev/null
+++ b/doc/programming_guide/time.rst
@@ -0,0 +1,176 @@
+Keeping track of time
+=====================
+
+pyglet's :py:mod:`~pyglet.clock` module allows you to schedule functions
+to run periodically, or for one-shot future execution.  There are also some
+helpful utilities provided for calculating and displaying the application
+frame rate.
+
+.. _guide_calling-functions-periodically:
+
+Calling functions periodically
+------------------------------
+
+As discussed in the :ref:`programming-guide-eventloop` section, pyglet
+applications begin execution by entering into an application event loop::
+
+    pyglet.app.run()
+
+Once called, this function doesn't return until the application windows have
+been closed.  This may leave you wondering how to execute code while the
+application is running.
+
+Typical applications need to execute code in only three circumstances:
+
+* A user input event (such as a mouse movement or key press) has been
+  generated.  In this case the appropriate code can be attached as an
+  event handler to the window.
+* An animation or other time-dependent system needs to update the position
+  or parameters of an object.  We'll call this a "periodic" event.
+* A certain amount of time has passed, perhaps indicating that an
+  operation has timed out, or that a dialog can be automatically dismissed.
+  We'll call this a "one-shot" event.
+
+To have a function called periodically, for example, once every 0.1 seconds::
+
+    def update(dt):
+        # ...
+    pyglet.clock.schedule_interval(update, 0.1)
+
+The `dt`, or `delta time` parameter gives the number of "wall clock" seconds
+elapsed since the last call of this function, (or the time the function was
+scheduled, if it's the first period). Due to latency, load and timer
+inprecision, this might be slightly more or less than the requested interval.
+Please note that the `dt` parameter is always passed to scheduled functions,
+so be sure to expect it when writing functions even if you don't need to
+use it.
+
+Scheduling functions with a set interval is ideal for animation, physics
+simulation, and game state updates.  pyglet ensures that the application does
+not consume more resources than necessary to execute the scheduled functions
+on time.
+
+Rather than "limiting the frame rate", as is common in other toolkits, simply
+schedule all your update functions for no less than the minimum period your
+application or game requires.  For example, most games need not run at more
+than 60Hz (60 times a second) for imperceptibly smooth animation, so the
+interval given to :py:func:`~pyglet.clock.schedule_interval` would be
+``1/60.0`` (or more).
+
+If you are writing a benchmarking program or otherwise wish to simply run at
+the highest possible frequency, use `schedule`. This will call the function
+as frequently as possible (and will likely cause heavy CPU usage)::
+
+    def benchmark(dt):
+        # ...
+    pyglet.clock.schedule(benchmark)
+
+By default pyglet window buffer swaps are synchronised to the display refresh
+rate, so you may also want to disable vsync if you are running a benchmark.
+
+For one-shot events, use :py:func:`~pyglet.clock.schedule_once`::
+
+    def dismiss_dialog(dt):
+        # ...
+
+    # Dismiss the dialog after 5 seconds.
+    pyglet.clock.schedule_once(dismiss_dialog, 5.0)
+
+To stop a scheduled function from being called, including cancelling a
+periodic function, use :py:func:`pyglet.clock.unschedule`. This could be
+useful if you want to start running a function on schedule when a user provides
+a certain input, and then unschedule it when another input is received.
+
+Sprite movement techniques
+--------------------------
+
+As mentioned above, every scheduled function receives a `dt` parameter,
+giving the actual "wall clock" time that passed since the previous invocation.
+This parameter can be used for numerical integration.
+
+For example, a non-accelerating particle with velocity ``v`` will travel
+some distance over a change in time ``dt``.  This distance is calculated as
+``v * dt``.  Similarly, a particle under constant acceleration ``a`` will have
+a change in velocity of ``a * dt``.
+
+The following example demonstrates a simple way to move a sprite across the
+screen at exactly 10 pixels per second::
+
+    sprite = pyglet.sprite.Sprite(image)
+    sprite.dx = 10.0
+
+    def update(dt):
+        sprite.x += sprite.dx * dt
+    pyglet.clock.schedule_interval(update, 1/60.0) # update at 60Hz
+
+This is a robust technique for simple sprite movement, as the velocity will
+remain constant regardless of the speed or load of the computer.
+
+Some examples of other common animation variables are given in the table
+below.
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Animation parameter
+          - Distance
+          - Velocity
+        * - Rotation
+          - Degrees
+          - Degrees per second
+        * - Position
+          - Pixels
+          - Pixels per second
+        * - Keyframes
+          - Frame number
+          - Frames per second
+
+The frame rate
+--------------
+
+Game performance is often measured in terms of the number of times the display
+is updated every second; that is, the frames-per-second or FPS.  You can
+determine your application's FPS with a single function call::
+
+    pyglet.clock.get_fps()
+
+The value returned is more useful than simply taking the reciprocal of `dt`
+from a period function, as it is averaged over a sliding window of several
+frames.
+
+Displaying the frame rate
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A simple way to profile your application performance is to display the frame
+rate while it is running.  Printing it to the console is not ideal as this
+will have a severe impact on performance.  pyglet provides the
+:py:class:`~pyglet.window.FPSDisplay` class for displaying the frame rate
+with very little effort::
+
+    fps_display = pyglet.window.FPSDisplay(window=window)
+
+    @window.event
+    def on_draw():
+        window.clear()
+        fps_display.draw()
+
+By default the frame rate will be drawn in the bottom-left corner of the
+window in a semi-translucent large font.
+See the :py:class:`~pyglet.window.FPSDisplay` documentation for details
+on how to customise this, or even display another clock value (such as
+the current time) altogether.
+
+User-defined clocks
+-------------------
+
+The default clock used by pyglet uses the system clock to determine the time
+(i.e., ``time.time()``).  Separate clocks can be created, however, allowing
+you to use another time source.  This can be useful for implementing a
+separate "game time" to the real-world time, or for synchronising to a network
+time source or a sound device.
+
+Each of the ``clock_*`` functions are aliases for the methods on a global
+instance of :py:class:`~pyglet.clock.Clock`. You can construct or subclass
+your own :py:class:`~pyglet.clock.Clock`, which can then maintain its own
+schedule and framerate calculation.
+See the class documentation for more details.
diff --git a/doc/programming_guide/windowing.rst b/doc/programming_guide/windowing.rst
new file mode 100644
index 0000000..b53e736
--- /dev/null
+++ b/doc/programming_guide/windowing.rst
@@ -0,0 +1,401 @@
+Windowing
+=========
+
+A :py:class:`~pyglet.window.Window` in pyglet corresponds to a top-level
+window provided by the operating system.  Windows can be floating
+(overlapped with other application windows) or fullscreen.
+
+.. _guide_creating-a-window:
+
+Creating a window
+-----------------
+
+If the :py:class:`~pyglet.window.Window` constructor is called with no
+arguments, defaults will be assumed for all parameters::
+
+    window = pyglet.window.Window()
+
+The default parameters used are:
+
+* The window will have a size of 640x480, and not be resizable.
+* A default context will be created using template config described in
+  :ref:`guide_glconfig`.
+* The window caption will be the name of the executing Python script
+  (i.e., ``sys.argv[0]``).
+
+Windows are visible as soon as they are created, unless you give the
+``visible=False`` argument to the constructor.  The following
+example shows how to create and display a window in two steps::
+
+    window = pyglet.window.Window(visible=False)
+    # ... perform some additional initialisation
+    window.set_visible()
+
+Context configuration
+^^^^^^^^^^^^^^^^^^^^^
+
+The context of a window cannot be changed once created.  There are several
+ways to control the context that is created:
+
+* Supply an already-created :py:class:`~pyglet.gl.Context` using the
+  ``context`` argument::
+
+      context = config.create_context(share)
+      window = pyglet.window.Window(context=context)
+
+* Supply a complete :py:class:`~pyglet.gl.Config` obtained from a
+  :py:class:`~pyglet.canvas.Screen` using the ``config``
+  argument.  The context will be created from this config and will share object
+  space with the most recently created existing context::
+
+      config = screen.get_best_config(template)
+      window = pyglet.window.Window(config=config)
+
+* Supply a template :py:class:`~pyglet.gl.Config` using the ``config``
+  argument. The context will use the best config obtained from the default
+  screen of the default display::
+
+      config = gl.Config(double_buffer=True)
+      window = pyglet.window.Window(config=config)
+
+* Specify a :py:class:`~pyglet.canvas.Screen` using the ``screen`` argument.
+  The context will use a config created from default template configuration
+  and this screen::
+
+      screen = display.get_screens()[screen_number]
+      window = pyglet.window.Window(screen=screen)
+
+* Specify a :py:class:`~pyglet.canvas.Display` using the ``display`` argument.
+  The default screen on this display will be used to obtain a context using
+  the default template configuration::
+
+      display = platform.get_display(display_name)
+      window = pyglet.window.Window(display=display)
+
+If a template :py:class:`~pyglet.gl.Config` is given, a
+:py:class:`~pyglet.canvas.Screen` or :py:class:`~pyglet.canvas.Display`
+may also be specified; however any other combination of parameters
+overconstrains the configuration and some parameters will be ignored.
+
+Fullscreen windows
+^^^^^^^^^^^^^^^^^^
+
+If the ``fullscreen=True`` argument is given to the window constructor, the
+window will draw to an entire screen rather than a floating window.  No window
+border or controls will be shown, so you must ensure you provide some other
+means to exit the application.
+
+By default, the default screen on the default display will be used, however
+you can optionally specify another screen to use instead.  For example, the
+following code creates a fullscreen window on the secondary screen::
+
+    screens = display.get_screens()
+    window = pyglet.window.Window(fullscreen=True, screen=screens[1])
+
+There is no way to create a fullscreen window that spans more than one window
+(for example, if you wanted to create an immersive 3D environment across
+multiple monitors).  Instead, you should create a separate fullscreen window
+for each screen and attach identical event handlers to all windows.
+
+Windows can be toggled in and out of fullscreen mode with the
+:py:meth:`~pyglet.window.Window.set_fullscreen`
+method.  For example, to return to windowed mode from fullscreen::
+
+    window.set_fullscreen(False)
+
+The previous window size and location, if any, will attempt to be restored,
+however the operating system does not always permit this, and the window may
+have relocated.
+
+Size and position
+-----------------
+
+This section applies only to windows that are not fullscreen.  Fullscreen
+windows always have the width and height of the screen they fill.
+
+You can specify the size of a window as the first two arguments to the window
+constructor.  In the following example, a window is created with a width of
+1280 pixels and a height of 720 pixels::
+
+    window = pyglet.window.Window(1280, 720)
+
+The "size" of a window refers to the drawable space within it, excluding any
+additional borders or title bar drawn by the operating system.
+
+You can allow the user to resize your window by specifying ``resizable=True``
+in the constructor.  If you do this, you may also want to handle the
+:py:meth:`~pyglet.window.Window.on_resize` event::
+
+    window = pyglet.window.Window(resizable=True)
+
+    @window.event
+    def on_resize(width, height):
+        print('The window was resized to %dx%d' % (width, height))
+
+You can specify a minimum and maximum size that the window can be resized to
+by the user with the :py:meth:`~pyglet.window.Window.set_minimum_size` and
+:py:meth:`~pyglet.window.Window.set_maximum_size` methods::
+
+    window.set_minimum_size(320, 200)
+    window.set_maximum_size(1024, 768)
+
+The window can also be resized programatically (even if the window is not
+user-resizable) with the :py:meth:`~pyglet.window.Window.set_size` method::
+
+    window.set_size(1280, 720)
+
+The window will initially be positioned by the operating system.  Typically,
+it will use its own algorithm to locate the window in a place that does not
+block other application windows, or cascades with them.  You can manually
+adjust the position of the window using the
+:py:meth:`~pyglet.window.Window.get_location` and
+:py:meth:`~pyglet.window.Window.set_location` methods::
+
+    x, y = window.get_location()
+    window.set_location(x + 20, y + 20)
+
+Note that unlike the usual coordinate system in pyglet, the window location is
+relative to the top-left corner of the desktop, as shown in the following
+diagram:
+
+.. figure:: img/window_location.png
+
+    The position and size of the window relative to the desktop.
+
+Appearance
+----------
+
+Window style
+^^^^^^^^^^^^
+
+Non-fullscreen windows can be created in one of four styles: default, dialog,
+tool or borderless.  Examples of the appearances of each of these styles under
+Windows and Mac OS X 10.4 are shown below.
+
+    .. list-table::
+        :header-rows: 1
+
+        * - Style
+          - Windows
+          - Mac OS X
+        * - :py:attr:`~pyglet.window.Window.WINDOW_STYLE_DEFAULT`
+          - .. image:: img/window_xp_default.png
+          - .. image:: img/window_osx_default.png
+        * - :py:attr:`~pyglet.window.Window.WINDOW_STYLE_DIALOG`
+          - .. image:: img/window_xp_dialog.png
+          - .. image:: img/window_osx_dialog.png
+        * - :py:attr:`~pyglet.window.Window.WINDOW_STYLE_TOOL`
+          - .. image:: img/window_xp_tool.png
+          - .. image:: img/window_osx_tool.png
+        * - :py:attr:`~pyglet.window.Window.WINDOW_STYLE_TRANSPARENT`
+          - .. image:: img/window_xp_transparent.png
+          - <Not Implemented>
+        * - :py:attr:`~pyglet.window.Window.WINDOW_STYLE_OVERLAY`
+          - .. image:: img/window_xp_overlay.png
+          - <Not Implemented>
+
+Non-resizable variants of these window styles may appear slightly different
+(for example, the maximize button will either be disabled or absent).
+
+Besides the change in appearance, the window styles affect how the window
+behaves.  For example, tool windows do not usually appear in the task bar and
+cannot receive keyboard focus.  Dialog windows cannot be minimized. Overlay's
+require custom sizing and moving of the respective window.
+the appropriate window style for your windows means your application will
+behave correctly for the platform on which it is running, however that
+behaviour may not be consistent across Windows, Linux and Mac OS X.
+
+The appearance and behaviour of windows in Linux will vary greatly depending
+on the distribution, window manager and user preferences.
+
+Borderless windows (:py:attr:`~pyglet.window.Window.WINDOW_STYLE_BORDERLESS`)
+are not decorated by the operating system at all, and have no way to be resized
+or moved around the desktop.  These are useful for implementing splash screens
+or custom window borders.
+
+You can specify the style of the window in the
+:py:class:`~pyglet.window.Window` constructor.
+Once created, the window style cannot be altered::
+
+    window = pyglet.window.Window(style=pyglet.window.Window.WINDOW_STYLE_DIALOG)
+
+Caption
+^^^^^^^
+
+The window's caption appears in its title bar and task bar icon (on Windows
+and some Linux window managers).  You can set the caption during window
+creation or at any later time using the
+:py:meth:`~pyglet.window.Window.set_caption` method::
+
+    window = pyglet.window.Window(caption='Initial caption')
+    window.set_caption('A different caption')
+
+Icon
+^^^^
+
+The window icon appears in the title bar and task bar icon on Windows and
+Linux, and in the dock icon on Mac OS X.  Dialog and tool windows do not
+necessarily show their icon.
+
+Windows, Mac OS X and the Linux window managers each have their own preferred
+icon sizes:
+
+    Windows XP
+        * A 16x16 icon for the title bar and task bar.
+        * A 32x32 icon for the Alt+Tab switcher.
+    Mac OS X
+        * Any number of icons of resolutions 16x16, 24x24, 32x32, 48x48, 72x72
+          and 128x128.  The actual image displayed will be interpolated to the
+          correct size from those provided.
+    Linux
+        * No constraints, however most window managers will use a 16x16 and a
+          32x32 icon in the same way as Windows XP.
+
+The :py:meth:`~pyglet.window.Window.set_icon` method allows you to set any
+number of images as the icon.
+pyglet will select the most appropriate ones to use and apply them to
+the window.  If an alternate size is required but not provided, pyglet will
+scale the image to the correct size using a simple interpolation algorithm.
+
+The following example provides both a 16x16 and a 32x32 image as the window
+icon::
+
+    window = pyglet.window.Window()
+    icon1 = pyglet.image.load('16x16.png')
+    icon2 = pyglet.image.load('32x32.png')
+    window.set_icon(icon1, icon2)
+
+You can use images in any format supported by pyglet, however it is
+recommended to use a format that supports alpha transparency such as PNG.
+Windows .ico files are supported only on Windows, so their use is discouraged.
+Mac OS X .icons files are not supported at all.
+
+Note that the icon that you set at runtime need not have anything to do with
+the application icon, which must be encoded specially in the application
+binary (see `Self-contained executables`).
+
+Visibility
+----------
+
+Windows have several states of visibility.  Already shown is the
+:py:attr:`~pyglet.window.Window.visible` property which shows or hides
+the window.
+
+Windows can be minimized, which is equivalent to hiding them except that
+they still appear on the taskbar (or are minimised to the dock, on OS X).
+The user can minimize a window by clicking the appropriate button in the
+title bar.
+You can also programmatically minimize a window using the
+:py:class:`~pyglet.window.Window.minimize` method (there is also a
+corresponding :py:class:`~pyglet.window.Window.maximize` method).
+
+When a window is made visible the :py:meth:`~pyglet.window.Window.on_show`
+event is triggered.  When it is hidden the
+:py:meth:`~pyglet.window.Window.on_hide` event is triggered.
+On Windows and Linux these events
+will only occur when you manually change the visibility of the window or when
+the window is minimized or restored.  On Mac OS X the user can also hide or
+show the window (affecting visibility) using the Command+H shortcut.
+
+.. _guide_subclassing-window:
+
+Subclassing Window
+------------------
+
+A useful pattern in pyglet is to subclass :py:class:`~pyglet.window.Window` for
+each type of window you will display, or as your main application class.  There
+are several benefits:
+
+* You can load font and other resources from the constructor, ensuring the
+  OpenGL context has already been created.
+* You can add event handlers simply by defining them on the class.  The
+  :py:meth:`~pyglet.window.Window.on_resize` event will be called as soon as
+  the window is created (this
+  doesn't usually happen, as you must create the window before you can attach
+  event handlers).
+* There is reduced need for global variables, as you can maintain application
+  state on the window.
+
+The following example shows the same "Hello World" application as presented
+in :ref:`quickstart`, using a subclass of :py:class:`~pyglet.window.Window`::
+
+    class HelloWorldWindow(pyglet.window.Window):
+        def __init__(self):
+            super(HelloWorldWindow, self).__init__()
+
+            self.label = pyglet.text.Label('Hello, world!')
+
+        def on_draw(self):
+            self.clear()
+            self.label.draw()
+
+    if __name__ == '__main__':
+        window = HelloWorldWindow()
+        pyglet.app.run()
+
+This example program is located in
+``examples/programming_guide/window_subclass.py``.
+
+Windows and OpenGL contexts
+---------------------------
+
+Every window in pyglet has an associated OpenGL context.
+Specifying the configuration of this context has already been covered in
+:ref:`guide_creating-a-window`.
+Drawing into the OpenGL context is the only way to draw into the window's
+client area.
+
+Double-buffering
+^^^^^^^^^^^^^^^^
+
+If the window is double-buffered (i.e., the configuration specified
+``double_buffer=True``, the default), OpenGL commands are applied to a hidden
+back buffer. This back buffer can be brought to the front using the `flip`
+method. The previous front buffer then becomes the hidden back buffer
+we render to in the next frame. If you are using the standard `pyglet.app.run`
+or :py:class:`pyglet.app.EventLoop` event loop, this is taken care of
+automatically after each :py:meth:`~pyglet.window.Window.on_draw` event.
+
+If the window is not double-buffered, the
+:py:meth:`~pyglet.window.Window.flip`  operation is unnecessary,
+and you should remember only to call :py:func:`pyglet.gl.glFlush` to
+ensure buffered commands are executed.
+
+Vertical retrace synchronisation
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Double-buffering eliminates one cause of flickering: the user is unable to see
+the image as it is painted, only the final rendering.  However, it does introduce
+another source of flicker known as "tearing".
+
+Tearing becomes apparent when displaying fast-moving objects in an animation.
+The buffer flip occurs while the video display is still reading data from the
+framebuffer, causing the top half of the display to show the previous frame
+while the bottom half shows the updated frame.  If you are updating the
+framebuffer particularly quickly you may notice three or more such "tears" in
+the display.
+
+pyglet provides a way to avoid tearing by synchronising buffer flips to the
+video refresh rate.  This is enabled by default, but can be set or unset
+manually at any time with the :py:attr:`~pyglet.window.Window.vsync` (vertical
+retrace synchronisation)
+property.  A window is created with vsync initially disabled in the following
+example::
+
+    window = pyglet.window.Window(vsync=False)
+
+It is usually desirable to leave vsync enabled, as it results in flicker-free
+animation.  There are some use-cases where you may want to disable it, for
+example:
+
+* Profiling an application.  Measuring the time taken to perform an operation
+  will be affected by the time spent waiting for the video device to refresh,
+  which can throw off results.  You should disable vsync if you are measuring
+  the performance of your application.
+* If you cannot afford for your application to block.  If your application run
+  loop needs to quickly poll a hardware device, for example, you may want to
+  avoid blocking with vsync.
+
+Note that some older video cards do not support the required extensions to
+implement vsync; this will appear as a warning on the console but is otherwise
+ignored.
diff --git a/doc/requirements.txt b/doc/requirements.txt
new file mode 100644
index 0000000..6055c7a
--- /dev/null
+++ b/doc/requirements.txt
@@ -0,0 +1,3 @@
+Sphinx
+sphinx-rtd-theme
+pytest
diff --git a/examples/eglcontext.py b/examples/eglcontext.py
new file mode 100644
index 0000000..9bc4544
--- /dev/null
+++ b/examples/eglcontext.py
@@ -0,0 +1,77 @@
+from pyglet.libs.egl import egl as libegl
+from pyglet.libs.egl.egl import *
+
+
+_buffer_types = {EGL_SINGLE_BUFFER: "EGL_RENDER_BUFFER",
+                 EGL_BACK_BUFFER: "EGL_BACK_BUFFER",
+                 EGL_NONE: "EGL_NONE"}
+
+_api_types = {EGL_OPENGL_API: "EGL_OPENGL_API",
+              EGL_OPENGL_ES_API: "EGL_OPENGL_ES_API",
+              EGL_NONE: "EGL_NONE"}
+
+# Initialize a display:
+display = libegl.EGLNativeDisplayType()
+display_connection = libegl.eglGetDisplay(display)
+
+majorver = libegl.EGLint()
+minorver = libegl.EGLint()
+result = libegl.eglInitialize(display_connection, majorver, minorver)
+assert result == 1, "EGL Initialization Failed"
+egl_version = majorver.value, minorver.value
+print(f"EGL version: {egl_version}")
+
+# Get the number of configs:
+num_configs = libegl.EGLint()
+config_size = libegl.EGLint()
+result = libegl.eglGetConfigs(display_connection, None, config_size, num_configs)
+assert result == 1, "Failed to query Configs"
+
+print("Number of configs available: ", num_configs.value)
+
+# Choose a config:
+config_attribs = (EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
+                  EGL_BLUE_SIZE, 8,
+                  EGL_GREEN_SIZE, 8,
+                  EGL_RED_SIZE, 8,
+                  EGL_DEPTH_SIZE, 8,
+                  EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
+                  EGL_NONE)
+config_attrib_array = (libegl.EGLint * len(config_attribs))(*config_attribs)
+egl_config = libegl.EGLConfig()
+result = libegl.eglChooseConfig(display_connection, config_attrib_array, egl_config, 1, num_configs)
+assert result == 1, "Failed to choose Config"
+
+# Create a surface:
+pbufferwidth = 1
+pbufferheight = 1
+pbuffer_attribs = (EGL_WIDTH, pbufferwidth, EGL_HEIGHT, pbufferheight, EGL_NONE)
+pbuffer_attrib_array = (libegl.EGLint * len(pbuffer_attribs))(*pbuffer_attribs)
+surface = libegl.eglCreatePbufferSurface(display_connection, egl_config, pbuffer_attrib_array)
+print("Surface: ", surface)
+
+# Bind the API:
+result = libegl.eglBindAPI(libegl.EGL_OPENGL_API)
+assert result == 1, "Failed to bind EGL_OPENGL_API"
+
+# Create a context:
+context_attribs = (EGL_CONTEXT_MAJOR_VERSION, 2, EGL_NONE)
+context_attrib_array = (libegl.EGLint * len(context_attribs))(*context_attribs)
+context = libegl.eglCreateContext(display_connection, egl_config, None, context_attrib_array)
+print("Context: ", context)
+
+# Make context current:
+result = libegl.eglMakeCurrent(display_connection, surface, surface, context)
+assert result == 1, "Failed to make context current"
+
+error_code = libegl.eglGetError()
+assert error_code == EGL_SUCCESS, "EGL Error code {} returned".format(error_code)
+
+# Print some context details:
+buffer_type = libegl.EGLint()
+libegl.eglQueryContext(display_connection, context, EGL_RENDER_BUFFER, buffer_type)
+print("Buffer type: ", _buffer_types.get(buffer_type.value, "Unknown"))
+print("API type: ", _api_types.get(libegl.eglQueryAPI(), "Unknown"))
+
+# Terminate EGL:
+libegl.eglTerminate(display_connection)
diff --git a/examples/events/window_platform_event.py b/examples/events/window_platform_event.py
index 84b137d..b1c2dd8 100755
--- a/examples/events/window_platform_event.py
+++ b/examples/events/window_platform_event.py
@@ -53,7 +53,7 @@ _have_cocoa = _have_win32 = _have_xlib = False
 
 if pyglet.compat_platform.startswith('linux'):
     _have_xlib = True
-    from pyglet.window.xlib import *
+    from pyglet.window.xlib import XlibEventHandler, xlib
 
 elif pyglet.compat_platform == 'darwin':
     _have_cocoa = True
@@ -61,7 +61,7 @@ elif pyglet.compat_platform == 'darwin':
 
 elif pyglet.compat_platform in ('win32', 'cygwin'):
     _have_win32 = True
-    from pyglet.window.win32 import *
+    from pyglet.window.win32 import Win32EventHandler, WM_DISPLAYCHANGE
 
 
 # Subclass Window
diff --git a/examples/file_dialog.py b/examples/file_dialog.py
new file mode 100644
index 0000000..e4367cc
--- /dev/null
+++ b/examples/file_dialog.py
@@ -0,0 +1,173 @@
+import os as _os
+
+from concurrent.futures import ProcessPoolExecutor as _ProcessPoolExecutor
+
+from pyglet.event import EventDispatcher as _EventDispatcher
+
+"""File dialog classes for opening and saving files.
+
+This module provides example Dialog Windows for opening and
+saving files. These are made using the `tkinter` module, which
+is part of the Python standard library. Dialog Windows run in a
+background process to prevent any interference with your main
+application, and integrate using the standard pyglet Event
+framework.
+
+Note that these dialogs do not actually open or save any data
+to disk. They simply return one or more strings, which contain
+the final file paths that were selected or entered. You can
+then use this information in your main application to handle
+the disk IO. This is done by attaching an event handler to the
+dialog, which will receive the file path(s) as an argument.
+
+Create a `FileOpenDialog` instance, and attach a handler to it::
+
+    # Restrict to only showing ".png" and ".bmp" file types,  
+    # and allow selecting more than one file
+    open_dialog = FileOpenDialog(filetypes=[("PNG", ".png"), ("24-bit Bitmap", ".bmp")], multiple=True)
+
+    @open_dialog.event
+    def on_dialog_open(filenames):
+        print("list of selected filenames:", filenames)
+        # Your own code here to handle loading the file name(s).
+
+    # Show the Dialog whenever you need. This is non-blocking:
+    open_dialog.show()
+
+
+The `FileSaveDialog` works similarly::
+    
+    # Add a default file extension ".sav" to the file
+    save_as = FileSaveDialog(default_ext='.sav')
+
+    @save_as.event
+    def on_dialog_save(filename):
+        print("FILENAMES ON SAVE!", filename)
+        # Your own code here to handle saving the file name(s).
+
+    # Show the Dialog whenever you need. This is non-blocking:
+    open_dialog.show()
+"""
+
+
+class _Dialog(_EventDispatcher):
+    """Dialog base class
+
+    This base class sets up a ProcessPoolExecutor with a single
+    background Process. This allows the Dialog to display in
+    the background without blocking or interfering with the main
+    application Process. This also limits to a single open Dialog
+    at a time.
+    """
+
+    executor = _ProcessPoolExecutor(max_workers=1)
+    _dialog = None
+
+    @staticmethod
+    def _open_dialog(dialog):
+        import tkinter as tk
+        root = tk.Tk()
+        root.withdraw()
+        return dialog.show()
+
+    def open(self):
+        future = self.executor.submit(self._open_dialog, self._dialog)
+        future.add_done_callback(self._dispatch_event)
+
+    def _dispatch_event(self, future):
+        raise NotImplementedError
+
+
+class FileOpenDialog(_Dialog):
+    def __init__(self, title="Open File", initial_dir=_os.path.curdir, filetypes=None, multiple=False):
+        """
+        :Parameters:
+            `title` : str
+                The Dialog Window name. Defaults to "Open File".
+            `initial_dir` : str
+                The directory to start in.
+            `filetypes` : list of tuple
+                An optional list of tuples containing (name, extension) to filter by.
+                If none are given, all files will be shown and selectable.
+                For example: `[("PNG", ".png"), ("24-bit Bitmap", ".bmp")]`
+            `multiple` : bool
+                True if multiple files can be selected. Defaults to False.
+        """
+        from tkinter import filedialog
+        self._dialog = filedialog.Open(title=title,
+                                       initialdir=initial_dir,
+                                       filetypes=filetypes or (),
+                                       multiple=multiple)
+
+    def _dispatch_event(self, future):
+        self.dispatch_event('on_dialog_open', future.result())
+
+    def on_dialog_open(self, filenames):
+        """Event for filename choices"""
+
+
+class FileSaveDialog(_Dialog):
+    def __init__(self, title="Save As", initial_dir=_os.path.curdir, initial_file=None, filetypes=None, default_ext=""):
+        """
+        :Parameters:
+            `title` : str
+                The Dialog Window name. Defaults to "Save As".
+            `initial_dir` : str
+                The directory to start in.
+            `initial_file` : str
+                A default file name to be filled in. Defaults to None.
+            `filetypes` : list of tuple
+                An optional list of tuples containing (name, extension) to
+                filter to. If the `default_ext` argument is not given, this list
+                also dictactes the extension that will be added to the entered
+                file name. If a list of `filetypes` are not give, you can enter
+                any file name to save as.
+                For example: `[("PNG", ".png"), ("24-bit Bitmap", ".bmp")]`
+            `default_ext` : str
+                A default file extension to add to the file. This will override
+                the `filetypes` list if given, but will not override a manually
+                entered extension.
+        """
+        from tkinter import filedialog
+        self._dialog = filedialog.SaveAs(title=title,
+                                         initialdir=initial_dir,
+                                         initialfile=initial_file or (),
+                                         filetypes=filetypes or (),
+                                         defaultextension=default_ext)
+
+    def _dispatch_event(self, future):
+        self.dispatch_event('on_dialog_save', future.result())
+
+    def on_dialog_save(self, filename):
+        """Event for filename choice"""
+
+
+FileOpenDialog.register_event_type('on_dialog_open')
+FileSaveDialog.register_event_type('on_dialog_save')
+
+
+if __name__ == '__main__':
+
+    #########################################
+    # File Save Dialog example:
+    #########################################
+
+    save_as = FileSaveDialog(initial_file="test", filetypes=[("PNG", ".png"), ("24-bit Bitmap", ".bmp")])
+
+    @save_as.event
+    def on_dialog_save(filename):
+        print("FILENAMES ON SAVE!", filename)
+
+    save_as.open()
+
+    #########################################
+    # File Open Dialog example:
+    #########################################
+
+    open_dialog = FileOpenDialog(filetypes=[("PNG", ".png"), ("24-bit Bitmap", ".bmp")], multiple=True)
+
+    @open_dialog.event
+    def on_dialog_open(filename):
+        print("FILENAMES ON OPEN!", filename)
+
+    open_dialog.open()
diff --git a/examples/graphics/shapes.py b/examples/graphics/shapes.py
new file mode 100644
index 0000000..6defaf3
--- /dev/null
+++ b/examples/graphics/shapes.py
@@ -0,0 +1,61 @@
+"""
+Simple example showing some animated shapes
+"""
+import math
+import pyglet
+from pyglet import shapes
+
+
+class ShapesDemo(pyglet.window.Window):
+
+    def __init__(self, width, height):
+        super().__init__(width, height, "Shapes")
+        self.time = 0
+        self.batch = pyglet.graphics.Batch()
+
+        self.circle = shapes.Circle(360, 240, 100, color=(255, 225, 255), batch=self.batch)
+        self.circle.opacity = 127
+
+        # Rectangle with center as anchor
+        self.square = shapes.Rectangle(360, 240, 200, 200, color=(55, 55, 255), batch=self.batch)
+        self.square.anchor_x = 100
+        self.square.anchor_y = 100
+
+        # Large transparent rectangle
+        self.rectangle = shapes.Rectangle(0, 190, 720, 100, color=(255, 22, 20), batch=self.batch)
+        self.rectangle.opacity = 64
+
+        self.line = shapes.Line(0, 0, 0, 480, width=4, color=(200, 20, 20), batch=self.batch)
+
+        self.triangle = shapes.Triangle(10, 10, 190, 10, 100, 150, color=(10, 255, 10), batch=self.batch)
+        self.triangle.opacity = 175
+
+        self.arc = shapes.Arc(50, 300, radius=40, segments=25, angle=4, color=(255, 255, 255), batch=self.batch)
+
+        self.ellipse = shapes.Ellipse(600, 300, a=50, b=30, color=(255, 250, 45), batch=self.batch)
+
+    def on_draw(self):
+        """Clear the screen and draw shapes"""
+        self.clear()
+        self.batch.draw()
+
+    def update(self, delta_time):
+        """Animate the shapes"""
+        self.time += delta_time
+        self.square.rotation = self.time * 15
+        self.rectangle.y = 200 + math.sin(self.time) * 190
+        self.circle.radius = 175 + math.sin(self.time * 1.17) * 30
+        self.line.position = (
+            360 + math.sin(self.time * 0.81) * 360,
+            0,
+            360 + math.sin(self.time * 1.34) * 360,
+            480,
+        )
+        self.arc.rotation = self.time * 30
+        self.ellipse.b = abs(math.sin(self.time) * 100)
+
+
+if __name__ == "__main__":
+    demo = ShapesDemo(720, 480)
+    pyglet.clock.schedule_interval(demo.update, 1/30)
+    pyglet.app.run()
diff --git a/examples/input/tablet.py b/examples/input/tablet.py
index 8a90fe9..9e2f7d2 100755
--- a/examples/input/tablet.py
+++ b/examples/input/tablet.py
@@ -1,8 +1,3 @@
-#!/usr/bin/python
-# $Id:$
-
-from __future__ import print_function
-
 import pyglet
 
 window = pyglet.window.Window()
@@ -34,6 +29,7 @@ def on_text(text):
         canvas = tablets[i].open(window)
     except pyglet.input.DeviceException:
         print('Failed to open tablet %d on window' % index)
+        return
 
     print('Opened %s' % name)
 
@@ -46,8 +42,8 @@ def on_text(text):
         print('%s: on_leave(%r)' % (name, cursor))
 
     @canvas.event
-    def on_motion(cursor, x, y, pressure):
-        print('%s: on_motion(%r, %r, %r, %r)' % (name, cursor, x, y, pressure))
+    def on_motion(cursor, x, y, pressure, tilt_x, tilt_y):
+        print('%s: on_motion(%r, %r, %r, %r, %r, %r)' % (name, cursor, x, y, pressure, tilt_x, tilt_y))
 
 
 @window.event
diff --git a/examples/text/advanced_font.py b/examples/text/advanced_font.py
new file mode 100644
index 0000000..2a17133
--- /dev/null
+++ b/examples/text/advanced_font.py
@@ -0,0 +1,49 @@
+
+"""Example of advanced font rendering features. Currently only supported on Windows."""
+
+import pyglet
+pyglet.options["advanced_font_features"] = True
+
+if pyglet.compat_platform != 'win32':
+    print("This example is only for Windows")
+    exit()
+
+# On Windows, it's possible to change the font anti-aliasing mode. 
+# Uncomment the below lines to set the options:
+#
+# from pyglet.font.directwrite import DirectWriteGlyphRenderer
+# D2D1_TEXT_ANTIALIAS_MODE_DEFAULT = 0
+# D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE = 1
+# D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE = 2
+# D2D1_TEXT_ANTIALIAS_MODE_ALIASED = 3
+# DirectWriteGlyphRenderer.antialias_mode = D2D1_TEXT_ANTIALIAS_MODE_ALIASED
+
+window = pyglet.window.Window()
+batch = pyglet.graphics.Batch()
+
+arial_bold = pyglet.text.Label("Hello World 👽", font_name="Arial", bold=True, font_size=25, x=50, y=400, batch=batch)
+arial_black = pyglet.text.Label("Hello World 👾", font_name="Arial", bold="black", font_size=25, x=50, y=350, batch=batch)
+arial_narrow = pyglet.text.Label("Hello World 🤖", font_name="Arial", bold=False, stretch="condensed", font_size=25, x=50, y=300, batch=batch)
+arial = pyglet.text.Label("Hello World 👀", font_name="Arial", font_size=25, x=50, y=250, batch=batch)
+
+segoe_ui_black = pyglet.text.Label("Hello World ☂️", font_name="Segoe UI", bold="black", font_size=25, x=50, y=200, batch=batch)
+segoe_ui_semilbold = pyglet.text.Label("Hello World ⚽️", font_name="Segoe UI", bold="semibold", font_size=25, x=50, y=150, batch=batch)
+segoe_ui_semilight = pyglet.text.Label("Hello World 🎱", font_name="Segoe UI", bold="semilight", font_size=25, x=50, y=100, batch=batch)
+segoe_ui_light = pyglet.text.Label("Hello World 🥳👍", font_name="Segoe UI", bold="light", font_size=25, x=50, y=50, batch=batch)
+segoe_ui = pyglet.text.Label("Hello World 😀✌", font_name="Segoe UI", font_size=25, x=50, y=10, batch=batch)
+
+# On Windows DirectWrite can render directly to an image for special cases!
+# Note: Labels are recommended unless you know what you are doing, or if you use these in a limited fashion.
+font = pyglet.font.load("Segoe UI")
+image = font.render_to_image("I am rendered as a texture! 🌎", 100, 300)
+sprite = pyglet.sprite.Sprite(image, x=400, y=400, batch=batch)
+sprite.rotation = 45
+
+
+@window.event
+def on_draw():
+    window.clear()
+    batch.draw()
+
+
+pyglet.app.run()
diff --git a/examples/camera.py b/examples/window/camera.py
similarity index 100%
rename from examples/camera.py
rename to examples/window/camera.py
diff --git a/examples/window/camera_group.py b/examples/window/camera_group.py
new file mode 100644
index 0000000..9419212
--- /dev/null
+++ b/examples/window/camera_group.py
@@ -0,0 +1,183 @@
+""" Camera class similar to that of camera.py, this time implemented as a graphics Group.
+Interface is much more simplified, with only a position and zoom implemented, but is easily
+extended to add other features such as autoscroll.
+
+    camera = CameraGroup(x=0, y=0, zoom=1)
+
+    world_object = pyglet.some_renderable(batch=batch, group=camera)
+    ui_object = pyglet.some_renderable(batch=batch)  # <-- Using the same batch here
+
+    @window.event
+    def on_draw():
+        window.clear()
+        batch.draw()  # Only one batch necessary
+
+A centered camera class is also provided, where the position of the camera is the center of
+the screen instead of the bottom left.
+
+    centered_camera = CenteredCameraGroup(window, x=0, y=0, zoom=1)
+
+Demo:
+
+Use arrow keys to move the camera around the scene.
+Note that everything in the window can be added to the same batch, as a group is used to
+seperate things in world space from things in "UI" space.
+"""
+
+import pyglet
+from pyglet.graphics import Group
+from pyglet.math import Vec2
+
+from typing import Optional
+
+
+class CameraGroup(Group):
+    """ Graphics group emulating the behaviour of a camera in 2D space. """
+
+    def __init__(
+        self,
+        x: float, y: float,
+        zoom: float = 1.0,
+        parent: Optional[Group] = None
+    ):
+        super().__init__(parent)
+        self.x, self.y = x, y
+        self.zoom = zoom
+
+    @property
+    def position(self) -> Vec2:
+        """Query the current offset."""
+        return Vec2(self.x, self.y)
+
+    @position.setter
+    def position(self, new_position: Vec2):
+        """Set the scroll offset directly."""
+        self.x, self.y = new_position
+
+    def set_state(self):
+        """ Apply zoom and camera offset to view matrix. """
+        pyglet.gl.glTranslatef(
+            -self.x * self.zoom,
+            -self.y * self.zoom,
+            0
+        )
+
+        # Scale with zoom
+        pyglet.gl.glScalef(self.zoom, self.zoom, 1)
+
+    def unset_state(self):
+        """ Revert zoom and camera offset from view matrix. """
+        # Since this is a matrix, you will need to reverse the translate after rendering otherwise
+        # it will multiply the current offset every draw update pushing it further and further away.
+
+        # Use inverse zoom to reverse zoom
+        pyglet.gl.glScalef(1 / self.zoom, 1 / self.zoom, 1)
+        # Reverse the translation
+        pyglet.gl.glTranslatef(
+            self.x * self.zoom,
+            self.y * self.zoom,
+            0
+        )
+
+
+class CenteredCameraGroup(CameraGroup):
+    """ Alternative centered camera group.
+
+    (0, 0) will be the center of the screen, as opposed to the bottom left.
+    """
+
+    def __init__(self, window: pyglet.window.Window, *args, **kwargs):
+        self.window = window
+        super().__init__(*args, **kwargs)
+
+    def set_state(self):
+        # Get our center offset, aka half the window dimensions
+        center_offset_x = self.window.width // 2
+        center_offset_y = self.window.height // 2
+
+        # Translate almost the same as normal, but add the center offset
+        pyglet.gl.glTranslatef(
+            -self.x * self.zoom + center_offset_x,
+            -self.y * self.zoom + center_offset_y,
+            0
+        )
+
+        # Scale like normal
+        pyglet.gl.glScalef(self.zoom, self.zoom, 1)
+
+    def unset_state(self):
+        # Get our center offset, aka half the window dimensions, because we are reversing the transform
+        # we use the negative dimensions here.
+        center_offset_x = -self.window.width // 2
+        center_offset_y = -self.window.height // 2
+
+        pyglet.gl.glScalef(1 / self.zoom, 1 / self.zoom, 1)
+
+        # Reverse the translation including center offset
+        pyglet.gl.glTranslatef(
+            self.x * self.zoom + center_offset_x,
+            self.y * self.zoom + center_offset_y,
+            0
+        )
+
+
+if __name__ == "__main__":
+    from pyglet.window import key
+
+    # Create a window and a batch
+    window = pyglet.window.Window(resizable=True)
+    batch = pyglet.graphics.Batch()
+
+    # Key handler for movement
+    keys = key.KeyStateHandler()
+    window.push_handlers(keys)
+
+    # Use centered
+    camera = CenteredCameraGroup(window, 0, 0)
+    # Use un-centered
+    # camera = CameraGroup(0, 0)
+
+    # Create a scene
+    rect = pyglet.shapes.Rectangle(-25, -25, 50, 50, batch=batch, group=camera)
+    text = pyglet.text.Label("Text works too!", x=0, y=-50, anchor_x="center", batch=batch, group=camera)
+
+    # Create some "UI"
+    ui_text = pyglet.text.Label(
+        "Simply don't add to the group to make UI static (like this)",
+        anchor_y="bottom", batch=batch
+    )
+    position_text = pyglet.text.Label(
+        "",
+        x=window.width,
+        anchor_x="right", anchor_y="bottom",
+        batch=batch
+    )
+
+    @window.event
+    def on_draw():
+        # Draw our scene
+        window.clear()
+        batch.draw()
+
+    @window.event
+    def on_resize(width: float, height: float):
+        # Keep position text label to the right
+        position_text.x = width
+
+    def on_update(dt: float):
+        # Move camera with arrow keys
+        if keys[key.UP]:
+            camera.y += 50*dt
+        if keys[key.DOWN]:
+            camera.y -= 50*dt
+        if keys[key.LEFT]:
+            camera.x -= 50*dt
+        if keys[key.RIGHT]:
+            camera.x += 50*dt
+
+        # Update position text label
+        position_text.text = repr(round(camera.position))
+
+    # Start the demo
+    pyglet.clock.schedule(on_update)
+    pyglet.app.run()
diff --git a/examples/multiple_windows.py b/examples/window/multiple_windows.py
old mode 100755
new mode 100644
similarity index 100%
rename from examples/multiple_windows.py
rename to examples/window/multiple_windows.py
diff --git a/examples/window/overlay_window.py b/examples/window/overlay_window.py
new file mode 100644
index 0000000..959d78b
--- /dev/null
+++ b/examples/window/overlay_window.py
@@ -0,0 +1,23 @@
+"""Demonstrates creation of a transparent overlay window in pyglet
+"""
+
+import pyglet
+
+from pyglet.graphics import Batch
+from pyglet.window import Window
+
+
+batch = Batch()
+window = Window(500, 500, style=Window.WINDOW_STYLE_OVERLAY)
+window.set_caption("Overlay Window")
+
+circle = pyglet.shapes.Circle(250, 250, 100, color=(255, 255, 0), batch=batch)
+
+
+@window.event
+def on_draw():
+    window.clear()
+    batch.draw()
+
+
+pyglet.app.run()
diff --git a/examples/window/transparent_window.py b/examples/window/transparent_window.py
new file mode 100644
index 0000000..7461ff1
--- /dev/null
+++ b/examples/window/transparent_window.py
@@ -0,0 +1,23 @@
+"""Demonstrates creation of a transparent overlay window in pyglet
+"""
+
+import pyglet
+
+from pyglet.graphics import Batch
+from pyglet.window import Window
+
+
+batch = Batch()
+window = Window(500, 500, style=Window.WINDOW_STYLE_TRANSPARENT)
+window.set_caption("Transparent Window")
+
+circle = pyglet.shapes.Circle(250, 250, 100, color=(255, 255, 0), batch=batch)
+
+
+@window.event
+def on_draw():
+    window.clear()
+    batch.draw()
+
+
+pyglet.app.run()
diff --git a/experimental/dist_field/README b/experimental/dist_field/README
new file mode 100644
index 0000000..cd6f1d6
--- /dev/null
+++ b/experimental/dist_field/README
@@ -0,0 +1,30 @@
+Basic implementation of the paper "Improved Alpha-Tested Magnification for
+Vector Textures and Special Effects" by Chris Green, Valve.  
+http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf
+
+Run genfield.py over a large monochrome image (at least 1024x1024) to generate
+the compressed distance field image.  e.g.
+
+    python genfield.py --size=64 py.png
+
+Produces py.df.png (or use -o to specify output file).
+
+Then preview the alpha-tested rendering using renderfield.py
+
+    python renderfield.py py.df.png
+
+Enable pixel shader (requires OpenGL 2.0) with 'S' key.  Following keys modify
+the shader operation (and have no effect when shader is disabled):
+
+    A   Toggle antialiasing
+    B   Toggle bidirectional (see below)
+    O   Toggle outline
+    G   Toggle glow
+    ,   Decrease outline/glow
+    .   Increase outline/glow
+
+Bidirectional fields (see last page of Valve paper) can be generated by
+passing -b to genfield.py, and enabled in renderfield.py by pressing 'B'.
+Bidirectional field rendering requires shader, and seems to look worse in
+every case.  Glow, outline and AA have not (yet) been implemented for
+bidirectional.
diff --git a/experimental/dist_field/genfield.py b/experimental/dist_field/genfield.py
new file mode 100644
index 0000000..eeece17
--- /dev/null
+++ b/experimental/dist_field/genfield.py
@@ -0,0 +1,102 @@
+#!/usr/bin/python
+# $Id:$
+
+import ctypes
+import math
+import optparse
+import pyglet
+
+root2 = math.sqrt(2)
+
+def gen_dist_field(img, width, height, spread, bidirectional=True):
+    sample_width = img.width // width
+    sample_height = img.height // height
+    max_dist = spread * root2
+    dist_scale = 127. / max_dist
+
+    # Grab image data as list of int.
+    data = img.get_data('L', img.width)
+    if isinstance(data, str):
+        data = map(ord, data)
+
+    # Pad input image by `spread` on each side to avoid having to check bounds
+    # Add left and right sides
+    data_tmp = []
+    padx = [0] * spread
+    pitch = img.width
+    in_i = 0
+    for row_i in range(img.height):
+        data_tmp.extend(padx)
+        data_tmp.extend(data[in_i:in_i + pitch])
+        data_tmp.extend(padx)
+        in_i += pitch
+    pitch += spread * 2
+    # Add top and bottom
+    pady = [0] * (pitch * spread)
+    data = pady + data_tmp + pady
+
+    field = []
+    for y in range(height):
+        in_i = (spread + int((y + 0.5) * sample_height)) * pitch + \
+            spread + sample_width // 2
+        for x in range(width):
+            colour = data[in_i]
+            dist1_sq = dist2_sq = dist_sq = (max_dist + 1) ** 2
+            for sy in range(-spread, spread + 1):
+                row_i = in_i + sy * pitch
+                for sx in range(-spread, spread + 1):
+                    if data[row_i + sx] != colour:
+                        dist_sq = min(dist_sq, sy * sy + sx * sx)
+                        if sx * sy > 0:
+                            dist1_sq = min(dist1_sq, sy * sy + sx * sx)
+                        else:
+                            dist2_sq = min(dist2_sq, sy * sy + sx * sx)
+            dist = math.sqrt(dist_sq)
+            dist1 = math.sqrt(dist1_sq)
+            dist2 = math.sqrt(dist2_sq)
+
+            if not colour:
+                dist = -dist
+                dist1 = -dist1
+                dist2 = -dist2
+            dist = int(dist * dist_scale) + 128
+            dist1 = int(dist1 * dist_scale) + 128
+            dist2 = int(dist2 * dist_scale) + 128
+            if bidirectional:
+                field.append(dist1)
+                field.append(dist2)
+                field.append(dist)
+            else:
+                field.append(255)
+            field.append(dist)
+            in_i += sample_width
+
+    field = (ctypes.c_byte * len(field))(*field)
+
+    out_img = pyglet.image.create(width, height)
+    if bidirectional:
+        out_img.set_data('RGBA', width * 4, field)
+    else:
+        out_img.set_data('LA', width * 2, field)
+    return out_img
+
+if __name__ == '__main__':
+    import os
+    
+    parser = optparse.OptionParser()
+    parser.add_option('-s', '--size', type=int, default=32)
+    parser.add_option('-p', '--spread', type=int, default=128)
+    parser.add_option('-o', '--output', default=None)
+    parser.add_option('-b', '--bidirectional', action='store_true', 
+                      default=False)
+    (options, args) = parser.parse_args()
+
+    filename = args[0]
+    img = pyglet.image.load(filename)
+    img2 = gen_dist_field(img, options.size, options.size, 
+                          options.spread, options.bidirectional)
+    if not options.output:
+        base, _ = os.path.splitext(filename)
+        options.output = '%s.df.png' % base
+    img2.save(options.output)
+
diff --git a/experimental/dist_field/py.df.png b/experimental/dist_field/py.df.png
new file mode 100644
index 0000000..1ef24ad
Binary files /dev/null and b/experimental/dist_field/py.df.png differ
diff --git a/experimental/dist_field/py.png b/experimental/dist_field/py.png
new file mode 100644
index 0000000..bc98c43
Binary files /dev/null and b/experimental/dist_field/py.png differ
diff --git a/experimental/dist_field/renderfield.py b/experimental/dist_field/renderfield.py
new file mode 100644
index 0000000..d7c0a46
--- /dev/null
+++ b/experimental/dist_field/renderfield.py
@@ -0,0 +1,274 @@
+#!/usr/bin/python
+# $Id:$
+
+from __future__ import print_function
+import ctypes
+import sys
+
+import pyglet
+from pyglet.window import key
+from pyglet.gl import *
+
+class Shader:
+    '''Generic shader loader.'''
+    def __init__(self, vertex_source, fragment_source=None):
+        vertex_shader = self._create_shader(GL_VERTEX_SHADER, vertex_source)
+        if fragment_source:
+            fragment_shader = self._create_shader(GL_FRAGMENT_SHADER, 
+                                                  fragment_source)
+        
+        program = glCreateProgram()
+        glAttachShader(program, vertex_shader)
+        if fragment_source:
+            glAttachShader(program, fragment_shader)
+        glLinkProgram(program)
+
+        status = ctypes.c_int()
+        glGetProgramiv(program, GL_LINK_STATUS, status)
+        if not status.value:
+            length = ctypes.c_int()
+            glGetProgramiv(program, GL_INFO_LOG_LENGTH, length)
+            log = ctypes.c_buffer(length.value)
+            glGetProgramInfoLog(program, len(log), None, log)
+            print(log.value, file=sys.stderr)
+            raise RuntimeError('Program link error')
+
+        self.program = program
+        self._uniforms = {}
+
+    def _create_shader(self, type, source):
+        shader = glCreateShader(type)
+        c_source = ctypes.create_string_buffer(source)
+        c_source_ptr = ctypes.cast(ctypes.pointer(c_source), 
+                                   ctypes.POINTER(c_char))
+        glShaderSource(shader, 1, ctypes.byref(c_source_ptr), None)
+        glCompileShader(shader)
+
+        status = ctypes.c_int()
+        glGetShaderiv(shader, GL_COMPILE_STATUS, status)
+        if not status.value:
+            length = ctypes.c_int()
+            glGetShaderiv(shader, GL_INFO_LOG_LENGTH, length)
+            log = ctypes.c_buffer(length.value)
+            glGetShaderInfoLog(shader, len(log), None, log)
+            print(log.value, file=sys.stderr)
+            raise RuntimeError('Shader compile error')
+
+        return shader
+
+    def __getitem__(self, name):
+        try:
+            return self._uniforms[name]
+        except KeyError:
+            location = self._uniforms[name] = \
+                glGetUniformLocation(self.program, name)
+            return location
+
+class DistFieldTextureGroup(pyglet.sprite.SpriteGroup):
+    '''Override sprite's texture group to enable either the shader or alpha
+    testing.'''
+    def set_state(self):
+        glEnable(self.texture.target)
+        glBindTexture(self.texture.target, self.texture.id)
+
+        glPushAttrib(GL_COLOR_BUFFER_BIT)
+        if enable_shader:
+            glUseProgram(shader.program)
+            glUniform1i(shader['bidirectional'], enable_bidirectional)
+            glUniform1i(shader['antialias'], enable_antialias)
+            glUniform1i(shader['outline'], enable_outline)
+            glUniform1f(shader['outline_width'], outline_width)
+            glUniform1i(shader['glow'], enable_glow)
+            glUniform1f(shader['glow_width'], glow_width)
+            glEnable(GL_BLEND)
+            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+        else:
+            glEnable(GL_ALPHA_TEST)
+            glAlphaFunc(GL_GEQUAL, 0.5)
+
+    def unset_state(self):
+        if enable_shader:
+            glUseProgram(0)
+
+        glPopAttrib(GL_COLOR_BUFFER_BIT)
+        glDisable(self.texture.target)
+
+class DistFieldSprite(pyglet.sprite.Sprite):
+    '''Override sprite to use DistFieldTextureGroup.'''
+    def __init__(self, 
+                 img, x=0, y=0,
+                 blend_src=GL_SRC_ALPHA,
+                 blend_dest=GL_ONE_MINUS_SRC_ALPHA,
+                 batch=None,
+                 group=None,
+                 usage='dynamic'):
+        super(DistFieldSprite, self).__init__(
+            img, x, y, blend_src, blend_dest, batch, group, usage)
+        
+        self._group = DistFieldTextureGroup(
+            self._texture, blend_src, blend_dest, group)
+        self._usage = usage
+        self._create_vertex_list()
+
+window = pyglet.window.Window(resizable=True)
+
+@window.event
+def on_resize(width, height):
+    scale_width = width / float(image.width)
+    scale_height = height / float(image.height)
+    sprite.scale = min(scale_width, scale_height)
+    sprite.x = width / 2
+    sprite.y = height / 2
+
+@window.event
+def on_draw():
+    window.clear()
+    sprite.draw()
+
+@window.event
+def on_key_press(symbol, modifiers):
+    global enable_shader
+    global enable_bidirectional
+    global enable_antialias
+    global enable_outline
+    global enable_glow
+    global outline_width
+    global glow_width
+    if symbol == key.S:
+        enable_shader = not enable_shader
+    elif symbol == key.B:
+        enable_bidirectional = not enable_bidirectional
+    elif symbol == key.A:
+        enable_antialias = not enable_antialias
+    elif symbol == key.O:
+        enable_outline = not enable_outline
+        enable_glow = False
+    elif symbol == key.G:
+        enable_glow = not enable_glow
+        enable_outline = False
+    elif symbol == key.PERIOD:
+        if enable_glow:
+            glow_width += 0.005
+        else:
+            outline_width += 0.005
+    elif symbol == key.COMMA:
+        if enable_glow:
+            glow_width -= 0.005
+        else:
+            outline_width -= 0.005
+
+    print('-' * 78)
+    print('enable_shader', enable_shader)
+    print('enable_bidirectional', enable_bidirectional)
+    print('enable_antialias', enable_antialias)
+    print('enable_outline', enable_outline)
+    print('enable_glow', enable_glow)
+    print('outline_width', outline_width)
+
+image = pyglet.image.load(sys.argv[1])
+image.anchor_x = image.width // 2
+image.anchor_y = image.height // 2
+sprite = DistFieldSprite(image)
+    
+shader = Shader('''
+/* Vertex shader */
+void main() 
+{
+    /* Pass through */
+    gl_Position = ftransform();
+    gl_FrontColor = gl_Color;
+    gl_TexCoord[0] = gl_MultiTexCoord0;
+}
+''',
+'''
+/* Fragment shader */
+uniform sampler2D tex;
+
+uniform bool bidirectional;
+uniform bool antialias;
+uniform bool outline;
+uniform bool glow;
+uniform float outline_width;
+uniform float glow_width;
+const vec4 outline_color = vec4(0.0, 0.0, 1.0, 1.0);
+const vec4 glow_color = vec4(1.0, 0.0, 0.0, 1.0);
+
+void main()
+{
+    float alpha_mask;
+    if (bidirectional)
+    {
+        vec4 field = texture2D(tex, gl_TexCoord[0].st);
+        alpha_mask = float(field.r >= 0.5 && field.g >= 0.5);
+    }
+    else
+    {
+        alpha_mask = texture2D(tex, gl_TexCoord[0].st).a;
+    }
+    float alpha_width = fwidth(alpha_mask);
+    float intensity = alpha_mask;
+
+    gl_FragColor = gl_Color;
+
+    if (glow)
+    {
+        float glow_min = 0.5 - glow_width;
+        intensity = (alpha_mask - glow_min) / (0.5 - glow_min);
+        float glow_intensity = 0.0;
+        if (antialias)
+            glow_intensity = 1.0 - smoothstep(0.5 - alpha_width,
+                                              0.5 + alpha_width,
+                                              alpha_mask) * 2.0;
+        else
+            glow_intensity = float(alpha_mask < 0.5);
+
+
+        gl_FragColor = lerp(gl_FragColor, glow_color, glow_intensity);
+    }
+    else if (outline)
+    {
+        float outline_intensity = 0.0;
+        float outline_min = 0.5 - outline_width;
+        float outline_max = 0.5;
+        if (antialias)
+        {
+
+            outline_intensity = 1.0 - smoothstep(outline_max - alpha_width,
+                                                 outline_max + alpha_width,
+                                                 alpha_mask) * 2.0;
+                                        
+            intensity *= smoothstep(outline_min - alpha_width, 
+                                    outline_min + alpha_width, 
+                                    alpha_mask) * 2.0;
+        }
+        else
+        {
+            outline_intensity = 
+                float(alpha_mask >= outline_min && alpha_mask <= outline_max);
+            intensity = float(alpha_mask >= outline_min);
+        }
+        gl_FragColor = lerp(gl_FragColor, outline_color, outline_intensity);
+    }
+    else if (antialias) 
+    {
+        intensity *= smoothstep(0.5 - alpha_width, 
+                                0.5 + alpha_width, 
+                                alpha_mask) * 2.0;
+    }
+    else
+    {
+        intensity = float(alpha_mask >= 0.5);
+    }
+
+    gl_FragColor.a = intensity;
+}
+''')
+enable_shader = True
+enable_bidirectional = False
+enable_antialias = False
+enable_outline = False
+enable_glow = False
+outline_width = 0.02
+glow_width = 0.1
+
+pyglet.app.run()
diff --git a/experimental/hello_world.svg b/experimental/hello_world.svg
new file mode 100644
index 0000000..6e159e7
--- /dev/null
+++ b/experimental/hello_world.svg
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="744.09448819"
+   height="1052.3622047"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.43"
+   sodipodi:docbase="/home/richard/src/pyglet.googlecode.com/trunk/examples"
+   sodipodi:docname="hello_world.svg">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.98994949"
+     inkscape:cx="375"
+     inkscape:cy="479.5939"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     inkscape:window-width="751"
+     inkscape:window-height="540"
+     inkscape:window-x="674"
+     inkscape:window-y="432" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.25pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 91.428571,449.50504 L 88.571429,569.50504"
+       id="path1307" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.25pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 88.571429,509.50504 L 128.57143,509.50504"
+       id="path1309" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.25pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 131.42857,458.07647 L 134.28571,569.50504"
+       id="path1311" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.25pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 178.797,541.225 C 186.62568,541.47754 203.0394,539.1795 203.04066,528.09302 C 203.04192,518.0167 195.21198,510.66789 181.82746,510.92042 C 169.45272,511.1539 155.77084,514.46637 154.55334,535.16408 C 153.33583,555.86179 161.6244,566.98389 182.83761,567.48896 C 204.05081,567.99404 204.55589,562.18566 208.09143,560.4179"
+       id="path1315"
+       sodipodi:nodetypes="czzzzz" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.25pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 227.28432,465.46355 L 228.29447,566.4788"
+       id="path1317" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.25pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 250.51783,459.40264 L 254.55844,571.52957"
+       id="path1319" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.25pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 293.95439,513.95088 C 260.11683,512.88971 257.59498,564.15067 294.96454,565.46866 C 332.31215,566.78587 327.77536,515.01153 293.95439,513.95088 z "
+       id="path1321"
+       sodipodi:nodetypes="czz" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.25pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 97.984794,595.77324 C 97.984794,595.77324 84.342094,676.73832 112.12693,677.59559 C 139.91176,678.45286 137.38074,661.43316 137.38074,661.43316 C 137.38074,661.43316 132.97647,675.5753 165.66502,675.57529 C 198.35357,675.57528 183.84776,595.77324 183.84776,595.77324"
+       id="path1323"
+       sodipodi:nodetypes="czczc" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.25pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 271.73103,674.56513 L 270.72088,632.13872"
+       id="path1327" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.25pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 271.73103,647.29101 C 271.73103,647.29101 274.76149,631.12857 291.93408,632.13872 C 309.10668,633.14887 310.11683,645.27071 310.11683,645.27071"
+       id="path1329" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.27549551pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 324.25896,594.79658 L 325.26912,674.53164"
+       id="path1331" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.25pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 369.71583,651.33163 C 369.71583,631.63366 336.38079,619.00675 336.38079,650.32147 C 336.38079,680.62605 369.71583,672.06438 369.71583,651.33163 z "
+       id="path1333"
+       sodipodi:nodetypes="czz" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.25pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 371.73613,600.824 L 368.70567,673.55498"
+       id="path1335" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.25pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 230.80836,625.56851 C 196.9708,624.50734 194.44895,675.7683 231.81851,677.08629 C 269.16612,678.4035 264.62933,626.62916 230.80836,625.56851 z "
+       id="path1337"
+       sodipodi:nodetypes="czz" />
+  </g>
+</svg>
diff --git a/experimental/svg_test.py b/experimental/svg_test.py
new file mode 100644
index 0000000..28748eb
--- /dev/null
+++ b/experimental/svg_test.py
@@ -0,0 +1,381 @@
+#!/usr/bin/env python
+# -*- coding: latin-1 -*-
+
+"""
+"""
+
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+import math
+import random
+import re
+import os.path
+import pyglet
+from pyglet.gl import *
+import xml.dom
+import xml.dom.minidom
+
+
+class SmoothLineGroup(pyglet.graphics.Group):
+    @staticmethod
+    def set_state():
+        glPushAttrib(GL_ENABLE_BIT)
+        glEnable(GL_LINE_SMOOTH)
+        glEnable(GL_BLEND)
+        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+        glHint(GL_LINE_SMOOTH_HINT, GL_DONT_CARE)
+
+    @staticmethod
+    def unset_state():
+        glPopAttrib()
+
+
+class Curve:
+    PATH_RE = re.compile(r'([MLHVCSQTAZ])([^MLHVCSQTAZ]+)', re.IGNORECASE)
+    INT = r'([+-]?\d+)'
+    FLOAT = r'(?:[\s,]*)([+-]?\d+(?:\.\d+)?)'
+
+    HANDLERS = {}
+
+    def handle(self, rx, types, HANDLERS=HANDLERS):
+        def register(function):
+            HANDLERS[self] = (rx and re.compile(rx), function, types)
+            return function
+
+        return register
+
+    def __init__(self, spec, batch):
+        self.batch = batch
+
+        self.start = None
+        self.current = None
+        self.min_x = self.min_y = self.max_x = self.max_y = None
+
+        for cmd, value in self.PATH_RE.findall(spec):
+            # print (cmd, value)
+            if not cmd:
+                continue
+            rx, handler, types = self.HANDLERS[cmd.upper()]
+            if rx is None:
+                handler(self, cmd)
+            else:
+                v = []
+                for fields in rx.findall(value):
+                    v.append([types[i](e) for i, e in enumerate(fields)])
+                handler(self, cmd, v)
+
+    def _determine_rect(self, x, y):
+        y = -y
+        if self.min_x is None:
+            self.min_x = self.max_x = x
+            self.min_y = self.max_y = y
+        else:
+            if self.min_x > x:
+                self.min_x = x
+            elif self.max_x < x:
+                self.max_x = x
+            if self.min_y > y:
+                self.min_y = y
+            elif self.max_y < y:
+                self.max_y = y
+
+    @handle('M', FLOAT * 2, (float, float))
+    def moveto(self, cmd, points):
+        """Start a new sub-path at the given (x,y) coordinate. M (uppercase)
+        indicates that absolute coordinates will follow; m (lowercase)
+        indicates that relative coordinates will follow. If a relative moveto
+        (m) appears as the first element of the path, then it is treated as a
+        pair of absolute coordinates. If a moveto is followed by multiple pairs
+        of coordinates, the subsequent pairs are treated as implicit lineto
+        commands.
+
+        Parameters are (x y)+
+        """
+        points = [list(map(float, point)) for point in points]
+        # XXX handle relative
+        # XXX confirm that we always reset start here
+        self.start = self.current = points[0]
+        if len(points) > 2:
+            self.lineto({'m': 'l', 'M': 'L'}[cmd], points[1:])
+
+    @handle('L', FLOAT * 2, (float, float))
+    def lineto(self, cmd, points):
+        """Draw a line from the current point to the given (x,y) coordinate
+        which becomes the new current point. L (uppercase) indicates that
+        absolute coordinates will follow; l (lowercase) indicates that relative
+        coordinates will follow. A number of coordinates pairs may be specified
+        to draw a polyline. At the end of the command, the new current point is
+        set to the final set of coordinates provided.
+
+        Parameters are (x y)+
+        """
+        l = []
+        self._determine_rect(*self.current)
+        for point in points:
+            cx, cy = self.current
+            x, y = list(map(float, point))
+            l.extend([cx, -cy])
+            l.extend([x, -y])
+            self.current = (x, y)
+            self._determine_rect(x, y)
+        self.batch.add(len(l) // 2, GL_LINES, SmoothLineGroup(), ('v2f', l))
+
+    @handle('H', FLOAT, (float,))
+    def horizontal_lineto(self, cmd, xvals):
+        """Draws a horizontal line from the current point (cpx, cpy) to (x,
+        cpy). H (uppercase) indicates that absolute coordinates will follow; h
+        (lowercase) indicates that relative coordinates will follow. Multiple x
+        values can be provided (although usually this doesn't make sense). At
+        the end of the command, the new current point becomes (x, cpy) for the
+        final value of x.
+
+        Parameters are x+
+        """
+        cx, cy = self.current
+        self._determine_rect(*self.current)
+        x = float(xvals[-1])
+        self.batch.add(2, GL_LINES, None, ('v2f', (cx, -cy, x, -cy)))
+        self.current = (x, cy)
+        self._determine_rect(x, cy)
+
+    @handle('V', FLOAT, (float,))
+    def vertical_lineto(self, cmd, yvals):
+        """Draws a vertical line from the current point (cpx, cpy) to (cpx, y).
+        V (uppercase) indicates that absolute coordinates will follow; v
+        (lowercase) indicates that relative coordinates will follow. Multiple y
+        values can be provided (although usually this doesn't make sense). At
+        the end of the command, the new current point becomes (cpx, y) for the
+        final value of y.
+
+        Parameters are y+
+        """
+        cx, cy = self.current
+        self._determine_rect(*self.current)
+        y = float(yvals[-1])
+        self.batch.add(2, GL_LINES, None, ('v2f', [cx, -cy, cx, -y]))
+        self.current = (cx, y)
+        self._determine_rect(cx, y)
+
+    @handle('Z', None, None)
+    def closepath(self, cmd):
+        """Close the current subpath by drawing a straight line from the
+        current point to current subpath's initial point.
+        """
+        self.batch.add(2, GL_LINES, SmoothLineGroup(), ('v2f', self.current + tuple(self.start)))
+
+    @handle('C', FLOAT * 6, (float,) * 6)
+    def curveto(self, cmd, control_points):
+        """Draws a cubic Bézier curve from the current point to (x,y) using
+        (x1,y1) as the control point at the beginning of the curve and (x2,y2)
+        as the control point at the end of the curve. C (uppercase) indicates
+        that absolute coordinates will follow; c (lowercase) indicates that
+        relative coordinates will follow. Multiple sets of coordinates may be
+        specified to draw a polybézier. At the end of the command, the new
+        current point becomes the final (x,y) coordinate pair used in the
+        polybézier.
+
+        Control points are (x1 y1 x2 y2 x y)+
+        """
+        l = []
+        last = None
+        for entry in control_points:
+            x1, y1, x2, y2, x, y = list(map(float, entry))
+            t = 0
+            cx, cy = self.current
+            self.last_control = (x2, y2)
+            self.current = (x, y)
+            x1 *= 3
+            x2 *= 3
+            y1 *= 3
+            y2 *= 3
+            while t <= 1.01:
+                a = t
+                a2 = a ** 2
+                a3 = a ** 3
+                b = 1 - t
+                b2 = b ** 2
+                b3 = b ** 3
+                px = cx * b3 + x1 * b2 * a + x2 * b * a2 + x * a3
+                py = cy * b3 + y1 * b2 * a + y2 * b * a2 + y * a3
+                if last is not None:
+                    l.extend(last)
+                    l.extend((px, -py))
+                last = (px, -py)
+                self._determine_rect(px, py)
+                t += 0.01
+        self.batch.add(len(l) // 2, GL_LINES, SmoothLineGroup(), ('v2f', l))
+
+    @handle('S', FLOAT * 4, (float,) * 4)
+    def smooth_curveto(self, cmd, control_points):
+        """Draws a cubic Bézier curve from the current point to (x,y). The
+        first control point is assumed to be the reflection of the second
+        control point on the previous command relative to the current point.
+        (If there is no previous command or if the previous command was not an
+        C, c, S or s, assume the first control point is coincident with the
+        current point.) (x2,y2) is the second control point (i.e., the control
+        point at the end of the curve). S (uppercase) indicates that absolute
+        coordinates will follow; s (lowercase) indicates that relative
+        coordinates will follow. Multiple sets of coordinates may be specified
+        to draw a polybézier. At the end of the command, the new current point
+        becomes the final (x,y) coordinate pair used in the polybézier.
+
+        Control points are (x2 y2 x y)+
+        """
+        assert self.last_control is not None, 'S must follow S or C'
+
+        l = []
+        last = None
+        for entry in control_points:
+            x2, y2, x, y = list(map(float, entry))
+
+            # Reflect last control point
+            cx, cy = self.current
+            lcx, lcy = self.last_control
+            dx, dy = cx - lcx, cy - lcy
+            x1, y1 = cx + dx, cy + dy
+
+            t = 0
+            cx, cy = self.current
+            self.last_control = (x2, y2)
+            self.current = (x, y)
+
+            x1 *= 3
+            x2 *= 3
+            y1 *= 3
+            y2 *= 3
+            while t <= 1.01:
+                a = t
+                a2 = a ** 2
+                a3 = a ** 3
+                b = 1 - t
+                b2 = b ** 2
+                b3 = b ** 3
+                px = cx * b3 + x1 * b2 * a + x2 * b * a2 + x * a3
+                py = cy * b3 + y1 * b2 * a + y2 * b * a2 + y * a3
+                if last is not None:
+                    l.extend(last)
+                    l.extend((px, -py))
+                last = (px, -py)
+                self._determine_rect(px, py)
+                t += 0.01
+        # degenerate vertices
+        self.batch.add(len(l) // 2, GL_LINES, SmoothLineGroup(), ('v2f', l))
+
+    @handle('Q', FLOAT * 4, (float,) * 4)
+    def quadratic_curveto(self, cmd, control_points):
+        """Draws a quadratic Bézier curve from the current point to (x,y)
+        using (x1,y1) as the control point. Q (uppercase) indicates that
+        absolute coordinates will follow; q (lowercase) indicates that
+        relative coordinates will follow. Multiple sets of coordinates may
+        be specified to draw a polybézier. At the end of the command, the
+        new current point becomes the final (x,y) coordinate pair used in
+        the polybézier.
+
+        Control points are (x1 y1 x y)+
+        """
+        raise NotImplementedError('not implemented')
+
+    @handle('T', FLOAT * 2, (float,) * 2)
+    def smooth_quadratic_curveto(self, cmd, control_points):
+        """Draws a quadratic Bézier curve from the current point to (x,y).
+        The control point is assumed to be the reflection of the control
+        point on the previous command relative to the current point. (If
+        there is no previous command or if the previous command was not a
+        Q, q, T or t, assume the control point is coincident with the
+        current point.) T (uppercase) indicates that absolute coordinates
+        will follow; t (lowercase) indicates that relative coordinates will
+        follow. At the end of the command, the new current point becomes
+        the final (x,y) coordinate pair used in the polybézier.
+
+        Control points are (x y)+
+        """
+        raise NotImplementedError('not implemented')
+
+    @handle('A', FLOAT * 3 + INT * 2 + FLOAT * 2, (float,) * 3 + (int,) * 2 + (float,) * 2)
+    def elliptical_arc(self, cmd, parameters):
+        """Draws an elliptical arc from the current point to (x, y). The
+        size and orientation of the ellipse are defined by two radii (rx,
+        ry) and an x-axis-rotation, which indicates how the ellipse as a
+        whole is rotated relative to the current coordinate system. The
+        center (cx, cy) of the ellipse is calculated automatically to
+        satisfy the constraints imposed by the other parameters.
+        large-arc-flag and sweep-flag contribute to the automatic
+        calculations and help determine how the arc is drawn.
+
+        Parameters are (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+
+        """
+        raise NotImplementedError('not implemented')
+
+
+class SVG:
+    def __init__(self, filename, rect=None):
+        self._rect = rect
+        self.scale_x = None
+        self.scale_y = None
+        self.translate_x = None
+        self.translate_y = None
+        dom = xml.dom.minidom.parse(filename)
+        tag = dom.documentElement
+        if tag.tagName != 'svg':
+            raise ValueError('document is <%s> instead of <svg>' % tag.tagName)
+
+        # generate all the drawing elements
+        self.batch = pyglet.graphics.Batch()
+        self.objects = []
+        for tag in tag.getElementsByTagName('g'):
+            for tag in tag.getElementsByTagName('path'):
+                self.objects.append(Curve(tag.getAttribute('d'), self.batch))
+
+        # determine drawing bounds
+        self.min_x = min(o.min_x for o in self.objects)
+        self.max_x = max(o.max_x for o in self.objects)
+        self.min_y = min(o.min_y for o in self.objects)
+        self.max_y = max(o.max_y for o in self.objects)
+
+        # determine or apply drawing rect
+        if rect is None:
+            self._rect = (self.min_x, self.min_y,
+                          self.max_x - self.min_x,
+                          self.max_y - self.min_y)
+
+    @property
+    def rect(self):
+        return self._rect
+
+    @rect.setter
+    def rect(self, new_rect):
+        self._rect = new_rect
+        # figure transform for display rect
+        self.translate_x, self.translate_y, rw, rh = new_rect
+        self.scale_x = abs(rw / float(self.max_x - self.min_x))
+        self.scale_y = abs(rh / float(self.max_y - self.min_y))
+
+    def draw(self):
+        glPushMatrix()
+        if self._rect is not None:
+            glScalef(self.scale_x, self.scale_y, 1)
+            glTranslatef(self.translate_x - self.min_x, self.translate_x - self.min_y, 0)
+        self.batch.draw()
+        glPopMatrix()
+
+
+window = pyglet.window.Window(width=600, height=300, resizable=True)
+
+dirname = os.path.dirname(__file__)
+svg = SVG(os.path.join(dirname, 'hello_world.svg'), rect=(0, 0, 600, 300))
+
+
+@window.event
+def on_draw():
+    window.clear()
+    svg.draw()
+
+
+@window.event
+def on_resize(w, h):
+    print("Resized")
+    svg.rect = svg.rect[:2] + (w, h)
+
+
+pyglet.app.run()
diff --git a/experimental/win32priority.py b/experimental/win32priority.py
new file mode 100644
index 0000000..d381bba
--- /dev/null
+++ b/experimental/win32priority.py
@@ -0,0 +1,68 @@
+# Utilies to patch Pyglet to provide more precise event timing on Windows.
+# HighTimerResolution is generally the most important of the bunch,
+# since most 60+fps games in Windows will not work right without
+# changing the timer resolution for the app
+
+import pyglet
+import ctypes
+
+try:
+    winmm = ctypes.windll.winmm
+    k32 = ctypes.windll.kernel32
+except AttributeError:
+    raise NotImplementedError("This is not a Win32 compatible platform.")
+
+
+class HighTimerResolution:
+    """
+    A context manager to adjust the main Win32 platform event loop.    
+    
+    Events inside the context manager will use the specified resolution of the
+    multimedia timer to provide proper timings for 60fps+ framerates.    
+    On exiting the context manager, the default resolution timer is restored.
+
+    Most 60fps games can use a resolution of 4, but you may need to go as low
+    as 1 to get the most consistent results.
+
+    Example:
+
+    with HighTimerResolution():
+        pyglet.app.run()
+
+    """
+
+    def __init__(self, resolution=1):
+        self.resolution = resolution
+
+    def __enter__(self):
+        winmm.timeBeginPeriod(self.resolution)
+
+    def __exit__(self, *a):
+        winmm.timeEndPeriod()
+
+
+def pin_thread(pin_thread_mask=0x01):
+    """
+    Pin the main Pyglet app thread to one or more cores by way of a thread affinity mask.
+    The default thread mask is 1.
+    """
+    k32.SetThreadAffinityMask(
+        pyglet.app.platform_event_loop._event_thread, pin_thread_mask
+    )
+
+
+def pin_process():
+    """
+    Pins the entire Python interpreter process to the first CPU core.
+    This may slightly improve performance due to affinity.
+    """
+    k32.SetProcessAffinityMask(k32.GetCurrentProcess(), 1)
+
+
+def raise_priority(
+    priority_class=0x080,
+):
+    """
+    Raises the process priority to High. Other priorities can be provided.
+    """
+    k32.SetPriorityClass(-1, priority_class)
diff --git a/make.py b/make.py
new file mode 100755
index 0000000..b0f4132
--- /dev/null
+++ b/make.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+import os
+import os.path as op
+import sys
+import shutil
+import webbrowser
+
+from subprocess import call
+
+THIS_DIR = op.dirname(op.abspath(__file__))
+DOC_DIR = op.join(THIS_DIR, 'doc')
+DIST_DIR = op.join(THIS_DIR, 'dist')
+GENDIST_TOOL = op.join(THIS_DIR, 'tools', 'gendist.sh')
+
+
+def clean():
+    """Clean up all build artifacts, including generated documentation."""
+    dirs = [op.join(DOC_DIR, '_build'),
+            op.join(DOC_DIR, 'api'),
+            DIST_DIR,
+            op.join(THIS_DIR, '_build'),
+            op.join(THIS_DIR, 'pyglet.egg-info')]
+    files = [op.join(DOC_DIR, 'internal', 'build.rst')]
+    for d in dirs:
+        print('   Removing:', d)
+        shutil.rmtree(d, ignore_errors=True)
+    for f in files:
+        print('   Removing:', f)
+        try:
+            os.remove(f)
+        except:
+            pass
+
+
+def docs():
+    """Generate documentation"""
+    make_bin = 'make.exe' if sys.platform == 'win32' else 'make'
+
+    html_dir = op.join(DOC_DIR, '_build', 'html')
+    if not op.exists(html_dir):
+        os.makedirs(op.join(DOC_DIR, '_build', 'html'))
+    call([make_bin, 'html'], cwd=DOC_DIR)
+    if '--open' in sys.argv:
+        webbrowser.open('file://' + op.abspath(DOC_DIR) + '/_build/html/index.html')
+
+
+def dist():
+    """Create all files to distribute Pyglet"""
+    docs()
+    call(GENDIST_TOOL)
+
+
+def _print_usage():
+    print('Usage:', op.basename(sys.argv[0]), '<command>')
+    print('  where commands are:', ', '.join(avail_cmds))
+    print()
+    for name, cmd in avail_cmds.items():
+        print(name, '\t', cmd.__doc__)
+
+
+if __name__ == '__main__':
+    avail_cmds = dict(clean=clean, dist=dist, docs=docs)
+    try:
+        command = avail_cmds[sys.argv[1]]
+    except IndexError:
+        # Invalid number of arguments, just print help
+        _print_usage()
+    except KeyError:
+        print('Unknown command:', sys.argv[1])
+        print()
+        _print_usage()
+    else:
+        command()
diff --git a/pyglet.egg-info/PKG-INFO b/pyglet.egg-info/PKG-INFO
index f646cd4..e68585a 100644
--- a/pyglet.egg-info/PKG-INFO
+++ b/pyglet.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: pyglet
-Version: 1.5.14
+Version: 1.5.24
 Summary: Cross-platform windowing and multimedia library
 Home-page: http://pyglet.readthedocs.org/en/latest/
 Author: Alex Holkner
@@ -10,150 +10,6 @@ Download-URL: http://pypi.python.org/pypi/pyglet
 Project-URL: Documentation, https://pyglet.readthedocs.io/en/latest
 Project-URL: Source, https://github.com/pyglet/pyglet
 Project-URL: Tracker, https://github.com/pyglet/pyglet/issues
-Description: [![pypi](https://badge.fury.io/py/pyglet.svg)](https://pypi.python.org/pypi/pyglet) [![rtd](https://readthedocs.org/projects/pyglet/badge/?version=latest)](https://pyglet.readthedocs.io)
-        
-        ![logo_large.png](https://bitbucket.org/repo/aejyXX/images/3385888514-logo_large.png)
-        
-        # pyglet
-        
-        *pyglet* is a cross-platform windowing and multimedia library for Python, intended for developing games
-        and other visually rich applications. It supports windowing, user interface event handling, Joysticks,
-        OpenGL graphics, loading images and videos, and playing sounds and music. *pyglet* works on Windows, OS X and Linux.
-        
-        * pyglet [documentation]
-        * pyglet [wiki]
-        * pyglet on [PyPI]
-        * pyglet [discord] server
-        * pyglet [mailing list]
-        * pyglet [issue tracker]
-        * pyglet [website]
-        
-        Pyglet has an active developer and user community.  If you find a bug or a problem with the documentation,
-        please [open an issue](https://github.com/pyglet/pyglet/issues).
-        Anyone is welcome to join our [discord] server where a lot of the development discussion is going on.
-        It's also a great place to ask for help.
-        
-        Some of the features of pyglet are:
-        
-        * **No external dependencies or installation requirements.** For most application and game requirements, *pyglet*
-          needs nothing else besides Python, simplifying distribution and installation. It's easy to package your project
-          with freezers such as PyInstaller. 
-        * **Take advantage of multiple windows and multi-monitor desktops.** *pyglet* allows you to use multiple
-          platform-native windows, and is fully aware of multi-monitor setups for use with fullscreen games.
-        * **Load images, sound, music and video in almost any format.** *pyglet* can optionally use FFmpeg to play back
-          audio formats such as MP3, OGG/Vorbis and WMA, and video formats such as MPEG2, H.264, H.265, WMV and Xvid.
-          Without FFmpeg, *pyglet* contains built-in support for standard formats such as wav, png, bmp, and others.
-        * **pyglet is written entirely in pure Python**, and makes use of the *ctypes* module to interface with system
-          libraries. You can modify the codebase or make a contribution without any second language compilation steps or
-          compiler setup. Despite being pure Python, *pyglet* has excellent performance thanks to advanced batching for
-          drawing thousands of objects.
-        * **pyglet is provided under the BSD open-source license**, allowing you to use it for both commercial and other
-          open-source projects with very little restriction.
-        
-        ## Requirements
-        
-        Pyglet runs under Python 3.5+. Being written in pure Python, it also works on other Python
-        interpreters such as PyPy. Supported platforms are:
-        
-        * Windows 7 or later
-        * Mac OS X 10.3 or later
-        * Linux, with the following libraries (most recent distributions will have
-          these in a default installation):
-          * OpenGL and GLX
-          * GDK 2.0+ or Pillow (required for loading images other than PNG and BMP)
-          * OpenAL or Pulseaudio (required for playing audio)
-        
-        **Please note that pyglet v1.5 will likely be the last version to support
-        legacy OpenGL**. Future releases of pyglet will be targeting OpenGL 3.3+.
-        Previous releases will remain available for download.
-        
-        Starting with version 1.4, to play compressed audio and video files,
-        you will also need [FFmpeg](https://ffmpeg.org/).
-        
-        ## Installation
-        
-        pyglet is installable from PyPI:
-        
-            pip install --upgrade --user pyglet
-        
-        ## Installation from source
-        
-        If you're reading this `README` from a source distribution, you can install pyglet with:
-        
-            python setup.py install --user
-        
-        You can also install the latest development version direct from Github using:
-        
-            pip install --upgrade --user https://github.com/pyglet/pyglet/archive/master.zip
-        
-        For local development install pyglet in editable mode:
-        
-        ```bash
-        # with pip
-        pip install -e .
-        # with setup.py
-        python setup.py develop
-        ```
-        
-        There are no compilation steps during the installation; if you prefer,
-        you can simply add this directory to your `PYTHONPATH` and use pyglet without
-        installing it. You can also copy pyglet directly into your project folder.
-        
-        ## Contributing
-        
-        **A good way to start contributing to a component of pyglet is by its documentation**. When studying the code you
-        are going to work with, also read the associated docs. If you don't understand the code with the help of the docs,
-        it is a sign that the docs should be improved.
-        
-        If you want to contribute to pyglet, we suggest the following:
-        
-        * Fork the [official repository](https://github.com/pyglet/pyglet/fork).
-        * Apply your changes to your fork.
-        * Submit a [pull request](https://github.com/pyglet/pyglet/pulls) describing the changes you have made.
-        * Alternatively you can create a patch and submit it to the issue tracker.
-        
-        When making a pull request, check that you have addressed its respective documentation, both within the code docstrings
-        and the programming guide (if applicable). It is very important to all of us that the documentation matches the latest
-        code and vice-versa.
-        
-        Consequently, an error in the documentation, either because it is hard to understand or because it doesn't match the
-        code, is a bug that deserves to be reported on a ticket.
-        
-        ## Building Docs
-        
-            pip install -r doc/requirements.txt
-            python setup.py build_sphinx
-        
-        Please check [the README.md file in the doc directory](doc/README.md) for more details.
-        
-        ## Testing
-        
-        pyglet makes use of pytest for its test suite.
-        
-        ```bash
-        pip install -r tests/requirements.txt --user
-        # Only run unittests
-        pytest tests/unit
-        ```
-        
-        Please check the [testing section in the development guide](https://pyglet.readthedocs.io/en/latest/internal/testing.html)
-        for more information about running and writing tests.
-        
-        ## Contact
-        
-        pyglet is developed by many individual volunteers, and there is no central point of contact. If you have a question
-        about developing with pyglet, or you wish to contribute, please join the [mailing list] or the [discord] server.
-        
-        For legal issues, please contact [Alex Holkner](mailto:Alex.Holkner@gmail.com).
-        
-        [discord]: https://discord.gg/QXyegWe
-        [mailing list]: http://groups.google.com/group/pyglet-users
-        [documentation]: https://pyglet.readthedocs.io
-        [wiki]:  https://github.com/pyglet/pyglet/wiki
-        [pypi]:  https://pypi.org/project/pyglet/
-        [website]: http://pyglet.org/
-        [issue tracker]: https://github.com/pyglet/pyglet/issues
-        
 Platform: UNKNOWN
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Environment :: MacOS X
@@ -165,10 +21,158 @@ Classifier: Operating System :: MacOS :: MacOS X
 Classifier: Operating System :: Microsoft :: Windows
 Classifier: Operating System :: POSIX :: Linux
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.5
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
 Classifier: Topic :: Games/Entertainment
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
 Description-Content-Type: text/markdown
+License-File: LICENSE
+License-File: NOTICE
+
+[![pypi](https://badge.fury.io/py/pyglet.svg)](https://pypi.python.org/pypi/pyglet) [![rtd](https://readthedocs.org/projects/pyglet/badge/?version=latest)](https://pyglet.readthedocs.io)
+
+![logo_large.png](https://bitbucket.org/repo/aejyXX/images/3385888514-logo_large.png)
+
+# pyglet
+
+*pyglet* is a cross-platform windowing and multimedia library for Python, intended for developing games
+and other visually rich applications. It supports windowing, user interface event handling, Joysticks,
+OpenGL graphics, loading images and videos, and playing sounds and music. *pyglet* works on Windows, OS X and Linux.
+
+* pyglet [documentation]
+* pyglet [wiki]
+* pyglet on [PyPI]
+* pyglet [discord] server
+* pyglet [mailing list]
+* pyglet [issue tracker]
+* pyglet [website]
+
+Pyglet has an active developer and user community.  If you find a bug or a problem with the documentation,
+please [open an issue](https://github.com/pyglet/pyglet/issues).
+Anyone is welcome to join our [discord] server where a lot of the development discussion is going on.
+It's also a great place to ask for help.
+
+Some features of pyglet are:
+
+* **No external dependencies or installation requirements.** For most application and game requirements, *pyglet*
+  needs nothing else besides Python, simplifying distribution and installation. It's easy to package your project
+  with freezers such as PyInstaller. 
+* **Take advantage of multiple windows and multi-monitor desktops.** *pyglet* allows you to use multiple
+  platform-native windows, and is fully aware of multi-monitor setups for use with fullscreen games.
+* **Load images, sound, music and video in almost any format.** *pyglet* can optionally use FFmpeg to play back
+  audio formats such as MP3, OGG/Vorbis and WMA, and video formats such as MPEG2, H.264, H.265, WMV and Xvid.
+  Without FFmpeg, *pyglet* contains built-in support for standard formats such as wav, png, bmp, and others.
+* **pyglet is written entirely in pure Python**, and makes use of the *ctypes* module to interface with system
+  libraries. You can modify the codebase or make a contribution without any second language compilation steps or
+  compiler setup. Despite being pure Python, *pyglet* has excellent performance thanks to advanced batching for
+  drawing thousands of objects.
+* **pyglet is provided under the BSD open-source license**, allowing you to use it for both commercial and other
+  open-source projects with very little restriction.
+
+## Requirements
+
+pyglet runs under Python 3.6+. Being written in pure Python, it also works on other Python
+interpreters such as PyPy. Supported platforms are:
+
+* Windows 7 or later
+* Mac OS X 10.3 or later
+* Linux, with the following libraries (most recent distributions will have
+  these in a default installation):
+  * OpenGL and GLX
+  * GDK 2.0+ or Pillow (required for loading images other than PNG and BMP)
+  * OpenAL or Pulseaudio (required for playing audio)
+
+**Please note that pyglet v1.5 will likely be the last version to support
+legacy OpenGL**. Future releases of pyglet will be targeting OpenGL 3.3+.
+Previous releases will remain available for download.
+
+Starting with version 1.4, to play compressed audio and video files,
+you will also need [FFmpeg](https://ffmpeg.org/).
+
+## Installation
+
+pyglet is installable from PyPI:
+
+    pip install --upgrade --user pyglet
+
+## Installation from source
+
+If you're reading this `README` from a source distribution, you can install pyglet with:
+
+    python setup.py install --user
+
+You can also install the latest development version direct from Github using:
+
+    pip install --upgrade --user https://github.com/pyglet/pyglet/archive/master.zip
+
+For local development install pyglet in editable mode:
+
+```bash
+# with pip
+pip install -e .
+# with setup.py
+python setup.py develop
+```
+
+There are no compilation steps during the installation; if you prefer,
+you can simply add this directory to your `PYTHONPATH` and use pyglet without
+installing it. You can also copy pyglet directly into your project folder.
+
+## Contributing
+
+**A good way to start contributing to a component of pyglet is by its documentation**. When studying the code you
+are going to work with, also read the associated docs. If you don't understand the code with the help of the docs,
+it is a sign that the docs should be improved.
+
+If you want to contribute to pyglet, we suggest the following:
+
+* Fork the [official repository](https://github.com/pyglet/pyglet/fork).
+* Apply your changes to your fork.
+* Submit a [pull request](https://github.com/pyglet/pyglet/pulls) describing the changes you have made.
+* Alternatively you can create a patch and submit it to the issue tracker.
+
+When making a pull request, check that you have addressed its respective documentation, both within the code docstrings
+and the programming guide (if applicable). It is very important to all of us that the documentation matches the latest
+code and vice-versa.
+
+Consequently, an error in the documentation, either because it is hard to understand or because it doesn't match the
+code, is a bug that deserves to be reported on a ticket.
+
+## Building Docs
+
+    pip install -r doc/requirements.txt
+    python setup.py build_sphinx
+
+Please check [the README.md file in the doc directory](doc/README.md) for more details.
+
+## Testing
+
+pyglet makes use of pytest for its test suite.
+
+```bash
+pip install -r tests/requirements.txt --user
+# Only run unittests
+pytest tests/unit
+```
+
+Please check the [testing section in the development guide](https://pyglet.readthedocs.io/en/latest/internal/testing.html)
+for more information about running and writing tests.
+
+## Contact
+
+pyglet is developed by many individual volunteers, and there is no central point of contact. If you have a question
+about developing with pyglet, or you wish to contribute, please join the [mailing list] or the [discord] server.
+
+For legal issues, please contact [Alex Holkner](mailto:Alex.Holkner@gmail.com).
+
+[discord]: https://discord.gg/QXyegWe
+[mailing list]: http://groups.google.com/group/pyglet-users
+[documentation]: https://pyglet.readthedocs.io
+[wiki]:  https://github.com/pyglet/pyglet/wiki
+[pypi]:  https://pypi.org/project/pyglet/
+[website]: http://pyglet.org/
+[issue tracker]: https://github.com/pyglet/pyglet/issues
+
+
diff --git a/pyglet.egg-info/SOURCES.txt b/pyglet.egg-info/SOURCES.txt
index e987d5c..a485b72 100644
--- a/pyglet.egg-info/SOURCES.txt
+++ b/pyglet.egg-info/SOURCES.txt
@@ -1,13 +1,178 @@
+.coveragerc
+.gitignore
+.readthedocs.yml
+.travis.yml
+DESIGN
 LICENSE
 MANIFEST.in
 NOTICE
 README.md
 RELEASE_NOTES
+make.py
+pyproject.toml
+pytest.ini
 setup.py
-examples/camera.py
+contrib/aseprite_codec/README
+contrib/aseprite_codec/asedemo.py
+contrib/aseprite_codec/aseprite.py
+contrib/aseprite_codec/running.ase
+contrib/logo3d/logo3d.jpg
+contrib/logo3d/logo3d.obj.zip
+contrib/logo3d/logo3d.wings
+contrib/model/examples/euclid.py
+contrib/model/examples/obj_test.py
+contrib/model/examples/rabbit.mtl
+contrib/model/examples/rabbit.obj
+contrib/model/examples/tree_test.py
+contrib/model/model/__init__.py
+contrib/model/model/geometric.py
+contrib/model/model/obj.py
+contrib/model/model/obj_batch.py
+contrib/toys/euclid.py
+contrib/toys/follow_mouse.py
+contrib/toys/primitives.py
+contrib/toys/thrust.py
+doc/Makefile
+doc/README.md
+doc/conf.py
+doc/external_resources.rst
+doc/index.rst
+doc/make.bat
+doc/requirements.txt
+doc/_static/favicon.ico
+doc/_static/logo.png
+doc/_static/logo5.svg
+doc/_static/relatedlogo.png
+doc/ext/README.md
+doc/ext/__init__.py
+doc/ext/docstrings.py
+doc/ext/theme/pyglet/layout.html
+doc/ext/theme/pyglet/theme.conf
+doc/ext/theme/pyglet/static/basic.css_t
+doc/ext/theme/pyglet/static/pyglet.css_t
+doc/internal/blacklist.rst
+doc/internal/contributing.rst
+doc/internal/dist.rst
+doc/internal/doc.rst
+doc/internal/generated.rst
+doc/internal/gl.rst
+doc/internal/media_logging_manual.rst
+doc/internal/media_manual.rst
+doc/internal/testing.rst
+doc/internal/virtualenv.rst
+doc/internal/wraptypes-class.svg
+doc/internal/wraptypes.rst
+doc/modules/app.rst
+doc/modules/canvas.rst
+doc/modules/clock.rst
+doc/modules/event.rst
+doc/modules/font.rst
+doc/modules/gl.rst
+doc/modules/gui.rst
+doc/modules/info.rst
+doc/modules/input.rst
+doc/modules/media.rst
+doc/modules/media_synthesis.rst
+doc/modules/pyglet.rst
+doc/modules/resource.rst
+doc/modules/shapes.rst
+doc/modules/sprite.rst
+doc/modules/window.rst
+doc/modules/window_key.rst
+doc/modules/window_mouse.rst
+doc/modules/graphics/allocation.rst
+doc/modules/graphics/index.rst
+doc/modules/graphics/vertexattribute.rst
+doc/modules/graphics/vertexbuffer.rst
+doc/modules/graphics/vertexdomain.rst
+doc/modules/image/animation.rst
+doc/modules/image/atlas.rst
+doc/modules/image/index.rst
+doc/modules/text/caret.rst
+doc/modules/text/document.rst
+doc/modules/text/index.rst
+doc/modules/text/layout.rst
+doc/programming_guide/advanced.rst
+doc/programming_guide/context.rst
+doc/programming_guide/debug.rst
+doc/programming_guide/eventloop.rst
+doc/programming_guide/events.rst
+doc/programming_guide/examplegame.rst
+doc/programming_guide/gl.rst
+doc/programming_guide/graphics.rst
+doc/programming_guide/image.rst
+doc/programming_guide/input.rst
+doc/programming_guide/installation.rst
+doc/programming_guide/keyboard.rst
+doc/programming_guide/media.rst
+doc/programming_guide/mouse.rst
+doc/programming_guide/options.rst
+doc/programming_guide/quickstart.rst
+doc/programming_guide/resources.rst
+doc/programming_guide/shapes.rst
+doc/programming_guide/text.rst
+doc/programming_guide/time.rst
+doc/programming_guide/windowing.rst
+doc/programming_guide/img/abstract_image.png
+doc/programming_guide/img/abstract_image.svg
+doc/programming_guide/img/buffer_image.png
+doc/programming_guide/img/buffer_image.svg
+doc/programming_guide/img/context_flow.png
+doc/programming_guide/img/context_flow.svg
+doc/programming_guide/img/cursor_mac_crosshair.png
+doc/programming_guide/img/cursor_mac_default.png
+doc/programming_guide/img/cursor_mac_hand.png
+doc/programming_guide/img/cursor_mac_no.png
+doc/programming_guide/img/cursor_mac_size_down.png
+doc/programming_guide/img/cursor_mac_size_left.png
+doc/programming_guide/img/cursor_mac_size_left_right.png
+doc/programming_guide/img/cursor_mac_size_right.png
+doc/programming_guide/img/cursor_mac_size_up.png
+doc/programming_guide/img/cursor_mac_size_up_down.png
+doc/programming_guide/img/cursor_mac_text.png
+doc/programming_guide/img/cursor_mac_wait.png
+doc/programming_guide/img/cursor_win_crosshair.png
+doc/programming_guide/img/cursor_win_default.png
+doc/programming_guide/img/cursor_win_hand.png
+doc/programming_guide/img/cursor_win_help.png
+doc/programming_guide/img/cursor_win_no.png
+doc/programming_guide/img/cursor_win_size.png
+doc/programming_guide/img/cursor_win_size_left_right.png
+doc/programming_guide/img/cursor_win_size_nesw.png
+doc/programming_guide/img/cursor_win_size_nwse.png
+doc/programming_guide/img/cursor_win_size_up_down.png
+doc/programming_guide/img/cursor_win_text.png
+doc/programming_guide/img/cursor_win_wait.png
+doc/programming_guide/img/cursor_win_wait_arrow.png
+doc/programming_guide/img/explosion.png
+doc/programming_guide/img/font_metrics.png
+doc/programming_guide/img/font_metrics.svg
+doc/programming_guide/img/image_classes.png
+doc/programming_guide/img/image_classes.svg
+doc/programming_guide/img/image_grid.png
+doc/programming_guide/img/image_grid.svg
+doc/programming_guide/img/image_sequence.png
+doc/programming_guide/img/image_sequence.svg
+doc/programming_guide/img/mouse_coordinates.png
+doc/programming_guide/img/mouse_coordinates.svg
+doc/programming_guide/img/screens.png
+doc/programming_guide/img/screens.svg
+doc/programming_guide/img/text_classes.png
+doc/programming_guide/img/text_classes.svg
+doc/programming_guide/img/window_location.png
+doc/programming_guide/img/window_location.svg
+doc/programming_guide/img/window_osx_default.png
+doc/programming_guide/img/window_osx_dialog.png
+doc/programming_guide/img/window_osx_tool.png
+doc/programming_guide/img/window_xp_default.png
+doc/programming_guide/img/window_xp_dialog.png
+doc/programming_guide/img/window_xp_overlay.png
+doc/programming_guide/img/window_xp_tool.png
+doc/programming_guide/img/window_xp_transparent.png
+examples/eglcontext.py
+examples/file_dialog.py
 examples/fixed_resolution.py
 examples/hello_world.py
-examples/multiple_windows.py
 examples/timer.py
 examples/3dmodel/logo3d.mtl
 examples/3dmodel/logo3d.obj
@@ -74,6 +239,7 @@ examples/game/version5/game/resources.py
 examples/game/version5/game/util.py
 examples/graphics/image_convert.py
 examples/graphics/image_display.py
+examples/graphics/shapes.py
 examples/gui/bar.png
 examples/gui/button_down.png
 examples/gui/button_hover.png
@@ -113,10 +279,24 @@ examples/programming_guide/hello_world.py
 examples/programming_guide/image_viewer.py
 examples/programming_guide/kitten.jpg
 examples/programming_guide/window_subclass.py
+examples/text/advanced_font.py
 examples/text/font_comparison.py
 examples/text/html_label.py
 examples/text/pyglet.png
 examples/text/text_input.py
+examples/window/camera.py
+examples/window/camera_group.py
+examples/window/multiple_windows.py
+examples/window/overlay_window.py
+examples/window/transparent_window.py
+experimental/hello_world.svg
+experimental/svg_test.py
+experimental/win32priority.py
+experimental/dist_field/README
+experimental/dist_field/genfield.py
+experimental/dist_field/py.df.png
+experimental/dist_field/py.png
+experimental/dist_field/renderfield.py
 pyglet/__init__.py
 pyglet/clock.py
 pyglet/com.py
@@ -141,6 +321,7 @@ pyglet/app/xlib.py
 pyglet/canvas/__init__.py
 pyglet/canvas/base.py
 pyglet/canvas/cocoa.py
+pyglet/canvas/headless.py
 pyglet/canvas/win32.py
 pyglet/canvas/xlib.py
 pyglet/canvas/xlib_vidmoderestore.py
@@ -148,6 +329,7 @@ pyglet/extlibs/__init__.py
 pyglet/extlibs/png.py
 pyglet/font/__init__.py
 pyglet/font/base.py
+pyglet/font/directwrite.py
 pyglet/font/fontconfig.py
 pyglet/font/freetype.py
 pyglet/font/freetype_lib.py
@@ -170,6 +352,7 @@ pyglet/gl/glx_info.py
 pyglet/gl/glxext_arb.py
 pyglet/gl/glxext_mesa.py
 pyglet/gl/glxext_nv.py
+pyglet/gl/headless.py
 pyglet/gl/lib.py
 pyglet/gl/lib_agl.py
 pyglet/gl/lib_glx.py
@@ -219,6 +402,10 @@ pyglet/libs/darwin/cocoapy/__init__.py
 pyglet/libs/darwin/cocoapy/cocoalibs.py
 pyglet/libs/darwin/cocoapy/cocoatypes.py
 pyglet/libs/darwin/cocoapy/runtime.py
+pyglet/libs/egl/__init__.py
+pyglet/libs/egl/egl.py
+pyglet/libs/egl/eglext.py
+pyglet/libs/egl/lib.py
 pyglet/libs/win32/__init__.py
 pyglet/libs/win32/constants.py
 pyglet/libs/win32/dinput.py
@@ -237,15 +424,18 @@ pyglet/media/buffered_logger.py
 pyglet/media/events.py
 pyglet/media/exceptions.py
 pyglet/media/instrumentation.py
+pyglet/media/mediathreads.py
 pyglet/media/player.py
 pyglet/media/synthesis.py
 pyglet/media/codecs/__init__.py
 pyglet/media/codecs/base.py
 pyglet/media/codecs/ffmpeg.py
 pyglet/media/codecs/gstreamer.py
+pyglet/media/codecs/pyogg.py
 pyglet/media/codecs/wave.py
 pyglet/media/codecs/wmf.py
 pyglet/media/codecs/ffmpeg_lib/__init__.py
+pyglet/media/codecs/ffmpeg_lib/compat.py
 pyglet/media/codecs/ffmpeg_lib/libavcodec.py
 pyglet/media/codecs/ffmpeg_lib/libavformat.py
 pyglet/media/codecs/ffmpeg_lib/libavutil.py
@@ -301,6 +491,7 @@ pyglet/window/cocoa/pyglet_textview.py
 pyglet/window/cocoa/pyglet_view.py
 pyglet/window/cocoa/pyglet_window.py
 pyglet/window/cocoa/systemcursor.py
+pyglet/window/headless/__init__.py
 pyglet/window/win32/__init__.py
 pyglet/window/xlib/__init__.py
 tests/__init__.py
@@ -435,6 +626,7 @@ tests/interactive/media/__init__.py
 tests/interactive/media/test_player.py
 tests/interactive/screenshots/committed/README
 tests/interactive/screenshots/session/README
+tests/interactive/screenshots/session/tests.interactive.graphics.test_multitexture.test_multitexture.001.png
 tests/interactive/screenshots/session/tests.interactive.image.test_image.test_bmp_loading[rgb_16bpp.bmp].001.png
 tests/interactive/screenshots/session/tests.interactive.image.test_image.test_bmp_loading[rgb_1bpp.bmp].001.png
 tests/interactive/screenshots/session/tests.interactive.image.test_image.test_bmp_loading[rgb_24bpp.bmp].001.png
@@ -492,4 +684,117 @@ tests/unit/media/test_sources.py
 tests/unit/media/test_synthesis.py
 tests/unit/text/__init__.py
 tests/unit/text/test_layout.py
-tools/inspect_font.py
\ No newline at end of file
+tools/al_info.py
+tools/gendist.sh
+tools/gengl.py
+tools/genwrappers.py
+tools/gl_info.py
+tools/inspect_font.py
+tools/license.py
+tools/wgl.h
+tools/ffmpeg/bokeh_timeline.py
+tools/ffmpeg/compare.py
+tools/ffmpeg/configure.py
+tools/ffmpeg/extractors.py
+tools/ffmpeg/fs.py
+tools/ffmpeg/mp.py
+tools/ffmpeg/mpexceptions.py
+tools/ffmpeg/playmany.py
+tools/ffmpeg/readme_ffmpeg_debbuging_branch.txt
+tools/ffmpeg/readme_run_tests.txt
+tools/ffmpeg/report.py
+tools/ffmpeg/reports.py
+tools/ffmpeg/retry_crashed.py
+tools/ffmpeg/run_test_suite.py
+tools/ffmpeg/summarize.py
+tools/ffmpeg/test_instrumentation.py
+tools/ffmpeg/timeline.py
+tools/wraptypes/__init__.py
+tools/wraptypes/cparser.py
+tools/wraptypes/ctypesparser.py
+tools/wraptypes/lex.py
+tools/wraptypes/preprocessor.py
+tools/wraptypes/wrap.py
+tools/wraptypes/yacc.py
+website/LICENSE.txt
+website/README.rst
+website/pyglet.lektorproject
+website/assets/favicon.ico
+website/assets/static/css/example-custom-styles.css
+website/assets/static/images/pyglet.png
+website/content/contents.lr
+website/content/eclipse.jpg
+website/content/404.html/contents.lr
+website/content/404.html/ngc-5793.jpg
+website/content/authors/contents.lr
+website/content/authors/benjamin/contents.lr
+website/content/authors/benjamin/pyglet.png
+website/content/blog/contents.lr
+website/content/blog/welcome-blog/contents.lr
+website/themes/lektor-icon/AUTHORS.txt
+website/themes/lektor-icon/CHANGELOG.md
+website/themes/lektor-icon/CONTRIBUTING.md
+website/themes/lektor-icon/LICENSE.txt
+website/themes/lektor-icon/NOTICE.txt
+website/themes/lektor-icon/README.md
+website/themes/lektor-icon/theme.ini
+website/themes/lektor-icon/assets/static/css/bootstrap.css
+website/themes/lektor-icon/assets/static/css/icomoon.css
+website/themes/lektor-icon/assets/static/css/magnific-popup.css
+website/themes/lektor-icon/assets/static/css/style.css
+website/themes/lektor-icon/assets/static/fonts/bootstrap/glyphicons-halflings-regular.ttf
+website/themes/lektor-icon/assets/static/fonts/bootstrap/glyphicons-halflings-regular.woff
+website/themes/lektor-icon/assets/static/fonts/bootstrap/glyphicons-halflings-regular.woff2
+website/themes/lektor-icon/assets/static/fonts/icomoon/icomoon.ttf
+website/themes/lektor-icon/assets/static/fonts/icomoon/icomoon.woff
+website/themes/lektor-icon/assets/static/images/Preloader_2.gif
+website/themes/lektor-icon/assets/static/images/placeholder_person.png
+website/themes/lektor-icon/assets/static/js/jquery-3.3.1.min.js
+website/themes/lektor-icon/assets/static/js/jquery.easing.min.js
+website/themes/lektor-icon/assets/static/js/jquery.magnific-popup.min.js
+website/themes/lektor-icon/assets/static/js/jquery.stellar.js
+website/themes/lektor-icon/assets/static/js/jquery.stellar.min.js
+website/themes/lektor-icon/assets/static/js/jquery.waypoints.min.js
+website/themes/lektor-icon/assets/static/js/magnific-popup-options.js
+website/themes/lektor-icon/assets/static/js/main-singlelayout.js
+website/themes/lektor-icon/assets/static/js/main.js
+website/themes/lektor-icon/flowblocks/content.ini
+website/themes/lektor-icon/flowblocks/gallery.ini
+website/themes/lektor-icon/flowblocks/gallery_item.ini
+website/themes/lektor-icon/flowblocks/member.ini
+website/themes/lektor-icon/flowblocks/mission.ini
+website/themes/lektor-icon/flowblocks/mission_tab.ini
+website/themes/lektor-icon/flowblocks/service.ini
+website/themes/lektor-icon/flowblocks/services.ini
+website/themes/lektor-icon/flowblocks/team.ini
+website/themes/lektor-icon/images/blog-index.png
+website/themes/lektor-icon/images/full-blog.png
+website/themes/lektor-icon/images/full-page.png
+website/themes/lektor-icon/images/gallery-404.png
+website/themes/lektor-icon/images/gallery-singlepage.png
+website/themes/lektor-icon/images/mainpage-screenshots.png
+website/themes/lektor-icon/images/responsive-layout.png
+website/themes/lektor-icon/models/404.ini
+website/themes/lektor-icon/models/author.ini
+website/themes/lektor-icon/models/authors.ini
+website/themes/lektor-icon/models/blog-post.ini
+website/themes/lektor-icon/models/blog.ini
+website/themes/lektor-icon/models/page.ini
+website/themes/lektor-icon/models/single-layout.ini
+website/themes/lektor-icon/templates/404.html
+website/themes/lektor-icon/templates/author.html
+website/themes/lektor-icon/templates/authors.html
+website/themes/lektor-icon/templates/blog-layout.html
+website/themes/lektor-icon/templates/blog-post.html
+website/themes/lektor-icon/templates/blog.html
+website/themes/lektor-icon/templates/layout.html
+website/themes/lektor-icon/templates/none.html
+website/themes/lektor-icon/templates/page.html
+website/themes/lektor-icon/templates/single-layout.html
+website/themes/lektor-icon/templates/blocks/content.html
+website/themes/lektor-icon/templates/blocks/gallery.html
+website/themes/lektor-icon/templates/blocks/mission.html
+website/themes/lektor-icon/templates/blocks/services.html
+website/themes/lektor-icon/templates/blocks/team.html
+website/themes/lektor-icon/templates/macros/blog.html
+website/themes/lektor-icon/templates/macros/pagination.html
\ No newline at end of file
diff --git a/pyglet/__init__.py b/pyglet/__init__.py
index 4b644e1..0ebf228 100644
--- a/pyglet/__init__.py
+++ b/pyglet/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -52,20 +52,12 @@ _is_pyglet_doc_run = hasattr(sys, "is_pyglet_doc_run") and sys.is_pyglet_doc_run
 #: The release version of this pyglet installation.
 #:
 #: Valid only if pyglet was installed from a source or binary distribution
-#: (i.e. not in a checked-out copy from SVN).
-#:
-#: Use setuptools if you need to check for a specific release version, e.g.::
-#:
-#:    >>> import pyglet
-#:    >>> from pkg_resources import parse_version
-#:    >>> parse_version(pyglet.version) >= parse_version('1.1')
-#:    True
-#:
-version = '1.5.14'
+#: (i.e. not cloned from Git).
+version = '1.5.24'
 
 
-if sys.version_info < (3, 5):
-    raise Exception('pyglet %s requires Python 3.5 or newer.' % version)
+if sys.version_info < (3, 6):
+    raise Exception('pyglet %s requires Python 3.6 or newer.' % version)
 
 
 # pyglet platform treats *BSD systems as Linux
@@ -170,6 +162,10 @@ options = {
     'xlib_fullscreen_override_redirect': False,
     'darwin_cocoa': True,
     'search_local_libs': True,
+    'advanced_font_features': False,
+    'headless': False,
+    'headless_device': 0,
+    'win32_disable_shaping': False,
 }
 
 _option_types = {
@@ -188,12 +184,15 @@ _option_types = {
     'debug_trace_flush': bool,
     'debug_win32': bool,
     'debug_x11': bool,
-    'ffmpeg_libs_win': tuple,
     'graphics_vbo': bool,
     'shadow_window': bool,
     'vsync': bool,
     'xsync': bool,
     'xlib_fullscreen_override_redirect': bool,
+    'advanced_font_features': bool,
+    'headless': bool,
+    'headless_device': int,
+    'win32_disable_shaping': bool
 }
 
 
diff --git a/pyglet/app/__init__.py b/pyglet/app/__init__.py
index 69b43c1..313fc08 100644
--- a/pyglet/app/__init__.py
+++ b/pyglet/app/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/app/base.py b/pyglet/app/base.py
index 1667672..a80499f 100644
--- a/pyglet/app/base.py
+++ b/pyglet/app/base.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -96,11 +96,13 @@ class PlatformEventLoop:
         """
         while True:
             try:
-                dispatcher, event, args = self._event_queue.get(False)
+                dispatcher, evnt, args = self._event_queue.get(False)
+                dispatcher.dispatch_event(evnt, *args)
             except queue.Empty:
                 break
-
-            dispatcher.dispatch_event(event, *args)
+            except ReferenceError:
+                # weakly-referenced object no longer exists
+                pass
 
     def notify(self):
         """Notify the event loop that something needs processing.
diff --git a/pyglet/app/cocoa.py b/pyglet/app/cocoa.py
index f345a2c..db5a7ef 100644
--- a/pyglet/app/cocoa.py
+++ b/pyglet/app/cocoa.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/app/win32.py b/pyglet/app/win32.py
index c854a09..c4cf879 100644
--- a/pyglet/app/win32.py
+++ b/pyglet/app/win32.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/app/xlib.py b/pyglet/app/xlib.py
index eb4fb6c..69f5875 100644
--- a/pyglet/app/xlib.py
+++ b/pyglet/app/xlib.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/canvas/__init__.py b/pyglet/canvas/__init__.py
index 0dd9a40..200d677 100755
--- a/pyglet/canvas/__init__.py
+++ b/pyglet/canvas/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -97,8 +97,12 @@ def get_display():
 if _is_pyglet_doc_run:
     from pyglet.canvas.base import Display, Screen, Canvas, ScreenMode
 else:
-    from pyglet import compat_platform
-    if compat_platform == 'darwin':
+    from pyglet import compat_platform, options
+    if options['headless']:
+        from pyglet.canvas.headless import HeadlessDisplay as Display
+        from pyglet.canvas.headless import HeadlessScreen as Screen
+        from pyglet.canvas.headless import HeadlessCanvas as Canvas
+    elif compat_platform == 'darwin':
         from pyglet.canvas.cocoa import CocoaDisplay as Display
         from pyglet.canvas.cocoa import CocoaScreen as Screen
         from pyglet.canvas.cocoa import CocoaCanvas as Canvas
diff --git a/pyglet/canvas/base.py b/pyglet/canvas/base.py
index 7e65ec1..7e210dd 100755
--- a/pyglet/canvas/base.py
+++ b/pyglet/canvas/base.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/canvas/cocoa.py b/pyglet/canvas/cocoa.py
index 3af2709..d9ab59e 100644
--- a/pyglet/canvas/cocoa.py
+++ b/pyglet/canvas/cocoa.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/canvas/headless.py b/pyglet/canvas/headless.py
new file mode 100644
index 0000000..28353a0
--- /dev/null
+++ b/pyglet/canvas/headless.py
@@ -0,0 +1,106 @@
+# ----------------------------------------------------------------------------
+# pyglet
+# Copyright (c) 2006-2008 Alex Holkner
+# Copyright (c) 2008-2022 pyglet contributors
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+#  * Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+#  * Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in
+#    the documentation and/or other materials provided with the
+#    distribution.
+#  * Neither the name of pyglet nor the names of its
+#    contributors may be used to endorse or promote products
+#    derived from this software without specific prior written
+#    permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# ----------------------------------------------------------------------------
+
+import pyglet
+import warnings
+
+from .base import Display, Screen, ScreenMode, Canvas
+
+
+from ctypes import *
+from pyglet.libs.egl import egl
+from pyglet.libs.egl import eglext
+
+
+class HeadlessDisplay(Display):
+
+    def __init__(self):
+        super().__init__()
+        # TODO: fix this placeholder:
+        self._screens = [HeadlessScreen(self, 0, 0, 1920, 1080)]
+
+        num_devices = egl.EGLint()
+        eglext.eglQueryDevicesEXT(0, None, byref(num_devices))
+        if num_devices.value > 0:
+            headless_device = pyglet.options['headless_device']
+            if headless_device < 0 or headless_device >= num_devices.value:
+                raise ValueError('Invalid EGL devide id: %d' % headless_device)
+            devices = (eglext.EGLDeviceEXT * num_devices.value)()
+            eglext.eglQueryDevicesEXT(num_devices.value, devices, byref(num_devices))
+            self._display_connection = eglext.eglGetPlatformDisplayEXT(
+                eglext.EGL_PLATFORM_DEVICE_EXT, devices[headless_device], None)
+        else:
+            warnings.warn('No device available for EGL device platform. Using native display type.')
+            display = egl.EGLNativeDisplayType()
+            self._display_connection = egl.eglGetDisplay(display)
+
+        egl.eglInitialize(self._display_connection, None, None)
+
+    def get_screens(self):
+        return self._screens
+
+    def __del__(self):
+        egl.eglTerminate(self._display_connection)
+
+
+class HeadlessCanvas(Canvas):
+    def __init__(self, display, egl_surface):
+        super().__init__(display)
+        self.egl_surface = egl_surface
+
+
+class HeadlessScreen(Screen):
+    def __init__(self, display, x, y, width, height):
+        super().__init__(display, x, y, width, height)
+
+    def get_matching_configs(self, template):
+        canvas = HeadlessCanvas(self.display, None)
+        configs = template.match(canvas)
+        # XXX deprecate
+        for config in configs:
+            config.screen = self
+        return configs
+
+    def get_modes(self):
+        pass
+
+    def get_mode(self):
+        pass
+
+    def set_mode(self, mode):
+        pass
+
+    def restore_mode(self):
+        pass
diff --git a/pyglet/canvas/win32.py b/pyglet/canvas/win32.py
index 7423aa5..2b04a64 100755
--- a/pyglet/canvas/win32.py
+++ b/pyglet/canvas/win32.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -129,7 +129,12 @@ class Win32ScreenMode(ScreenMode):
         self.height = mode.dmPelsHeight
         self.depth = mode.dmBitsPerPel
         self.rate = mode.dmDisplayFrequency
+        self.scaling = mode.dmDisplayFixedOutput
 
+    def __repr__(self):
+        return '%s(width=%r, height=%r, depth=%r, rate=%r, scaling=%r)' % (
+            self.__class__.__name__,
+            self.width, self.height, self.depth, self.rate, self.scaling)
 
 class Win32Canvas(Canvas):
     def __init__(self, display, hwnd, hdc):
diff --git a/pyglet/canvas/xlib.py b/pyglet/canvas/xlib.py
index bbc65c6..95802d2 100644
--- a/pyglet/canvas/xlib.py
+++ b/pyglet/canvas/xlib.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/canvas/xlib_vidmoderestore.py b/pyglet/canvas/xlib_vidmoderestore.py
index fbe4bce..4de035d 100644
--- a/pyglet/canvas/xlib_vidmoderestore.py
+++ b/pyglet/canvas/xlib_vidmoderestore.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/clock.py b/pyglet/clock.py
index 1985384..053db74 100644
--- a/pyglet/clock.py
+++ b/pyglet/clock.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -722,7 +722,7 @@ def schedule_interval_soft(func, interval, *args, **kwargs):
 def schedule_once(func, delay, *args, **kwargs):
     """Schedule ``func`` to be called once after ``delay`` seconds.
 
-    This function uses the fefault clock. ``delay`` can be a float. The
+    This function uses the default clock. ``delay`` can be a float. The
     arguments passed to ``func`` are ``dt`` (time since last function call),
     followed by any ``*args`` and ``**kwargs`` given here.
 
diff --git a/pyglet/com.py b/pyglet/com.py
index 1e692c0..97d5159 100644
--- a/pyglet/com.py
+++ b/pyglet/com.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -346,6 +346,13 @@ class COMObject:
 
             cls._pointers[itf] = ctypes.pointer(ctypes.pointer(vtbl))
 
+    @property
+    def pointers(self):
+        """Returns pointers to the implemented interfaces in this COMObject.  Read-only.
+
+        :type: dict
+        """
+        return self._pointers
 
 class Interface(metaclass=COMInterfaceMeta):
     _methods_ = []
@@ -354,9 +361,9 @@ class Interface(metaclass=COMInterfaceMeta):
 class IUnknown(metaclass=COMInterfaceMeta):
     """These methods are not implemented by default yet. Strictly for COM method ordering."""
     _methods_ = [
-        ('QueryInterface', STDMETHOD(REFIID, ctypes.c_void_p)),
-        ('AddRef', METHOD(ctypes.c_int)),
-        ('Release', METHOD(ctypes.c_int))
+        ('QueryInterface', STDMETHOD(ctypes.c_void_p, REFIID, ctypes.c_void_p)),
+        ('AddRef', METHOD(ctypes.c_int, ctypes.c_void_p)),
+        ('Release', METHOD(ctypes.c_int, ctypes.c_void_p))
     ]
 
 
diff --git a/pyglet/event.py b/pyglet/event.py
index a361479..573c630 100644
--- a/pyglet/event.py
+++ b/pyglet/event.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -157,7 +157,6 @@ import inspect
 from functools import partial
 from weakref import WeakMethod
 
-
 EVENT_HANDLED = True
 EVENT_UNHANDLED = None
 
@@ -302,6 +301,7 @@ class EventDispatcher:
                             return frame
                     except KeyError:
                         pass
+
         frame = find_frame()
 
         # No frame matched; no error.
@@ -390,7 +390,7 @@ class EventDispatcher:
             "You need to register events with the class method "
             "EventDispatcher.register_event_type('event_name')."
         )
-        assert event_type in self.event_types,\
+        assert event_type in self.event_types, \
             "%r not found in %r.event_types == %r" % (event_type, self, self.event_types)
 
         invoked = False
@@ -428,7 +428,8 @@ class EventDispatcher:
 
         return False
 
-    def _raise_dispatch_exception(self, event_type, args, handler, exception):
+    @staticmethod
+    def _raise_dispatch_exception(event_type, args, handler, exception):
         # A common problem in applications is having the wrong number of
         # arguments in an event handler.  This is caught as a TypeError in
         # dispatch_event but the error message is obfuscated.
@@ -457,21 +458,18 @@ class EventDispatcher:
             n_handler_args = max(n_handler_args, n_args)
 
         # Allow default values to overspecify arguments
-        if (n_handler_args > n_args and handler_defaults and
-            n_handler_args - len(handler_defaults) <= n_args):
+        if n_handler_args > n_args >= n_handler_args - len(handler_defaults) and handler_defaults:
             n_handler_args = n_args
 
         if n_handler_args != n_args:
             if inspect.isfunction(handler) or inspect.ismethod(handler):
-                descr = "'%s' at %s:%d" % (handler.__name__,
-                                           handler.__code__.co_filename,
-                                           handler.__code__.co_firstlineno)
+                descr = f"'{handler.__name__}' at {handler.__code__.co_filename}:{handler.__code__.co_firstlineno}"
             else:
                 descr = repr(handler)
 
-            raise TypeError("The '{0}' event was dispatched with {1} arguments, "
-                            "but your handler {2} accepts only {3} arguments.".format(
-                             event_type, len(args), descr, len(handler_args)))
+            raise TypeError(f"The '{event_type}' event was dispatched with {len(args)} arguments,\n"
+                            f"but your handler {descr} accepts only {n_handler_args} arguments.")
+
         else:
             raise exception
 
@@ -493,20 +491,23 @@ class EventDispatcher:
                 # ...
 
         """
-        if len(args) == 0:                      # @window.event()
+        if len(args) == 0:  # @window.event()
             def decorator(func):
-                name = func.__name__
-                self.set_handler(name, func)
+                func_name = func.__name__
+                self.set_handler(func_name, func)
                 return func
+
             return decorator
-        elif inspect.isroutine(args[0]):        # @window.event
+        elif inspect.isroutine(args[0]):  # @window.event
             func = args[0]
             name = func.__name__
             self.set_handler(name, func)
             return args[0]
-        elif isinstance(args[0], str):          # @window.event('on_resize')
+        elif isinstance(args[0], str):  # @window.event('on_resize')
             name = args[0]
+
             def decorator(func):
                 self.set_handler(name, func)
                 return func
+
             return decorator
diff --git a/pyglet/extlibs/__init__.py b/pyglet/extlibs/__init__.py
index a76da67..ae8bb4f 100644
--- a/pyglet/extlibs/__init__.py
+++ b/pyglet/extlibs/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/extlibs/png.py b/pyglet/extlibs/png.py
index 673b323..ef719c4 100644
--- a/pyglet/extlibs/png.py
+++ b/pyglet/extlibs/png.py
@@ -1,58 +1,4 @@
-# ----------------------------------------------------------------------------
-# pyglet
-# Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-#
-#  * Redistributions of source code must retain the above copyright
-#    notice, this list of conditions and the following disclaimer.
-#  * Redistributions in binary form must reproduce the above copyright
-#    notice, this list of conditions and the following disclaimer in
-#    the documentation and/or other materials provided with the
-#    distribution.
-#  * Neither the name of pyglet nor the names of its
-#    contributors may be used to endorse or promote products
-#    derived from this software without specific prior written
-#    permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
-# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
-# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
-# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
-# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-# POSSIBILITY OF SUCH DAMAGE.
-# ----------------------------------------------------------------------------
-
-# Retrieved from https://github.com/drj11/pypng
-# Revision: f5c4c76d81093b6c3f39f83b203f6832c496c110
-#
-# Pyglet Changelog
-# ----------------
-# * Removed shebang
-# * Added Pyglet license
-# * Converted to python_future
-
-# http://www.python.org/doc/2.2.3/whatsnew/node5.html
-from __future__ import generators
-from __future__ import division
-from __future__ import print_function
-from builtins import str
-from builtins import zip
-from builtins import map
-from builtins import range
-from builtins import object
-from functools import reduce
-from io import open
+#!/usr/bin/env python
 
 # png.py - PNG encoder/decoder in pure Python
 #
@@ -85,79 +31,113 @@ from io import open
 # SOFTWARE.
 
 """
-Pure Python PNG Reader/Writer
-
-This Python module implements support for PNG images (see PNG
-specification at http://www.w3.org/TR/2003/REC-PNG-20031110/ ). It reads
-and writes PNG files with all allowable bit depths
-(1/2/4/8/16/24/32/48/64 bits per pixel) and colour combinations:
-greyscale (1/2/4/8/16 bit); RGB, RGBA, LA (greyscale with alpha) with
-8/16 bits per channel; colour mapped images (1/2/4/8 bit).
-Adam7 interlacing is supported for reading and
-writing.  A number of optional chunks can be specified (when writing)
-and understood (when reading): ``tRNS``, ``bKGD``, ``gAMA``.
+The ``png`` module can read and write PNG files.
+
+Installation and Overview
+-------------------------
+
+``pip install pypng``
 
 For help, type ``import png; help(png)`` in your python interpreter.
 
-A good place to start is the :class:`Reader` and :class:`Writer`
-classes.
+A good place to start is the :class:`Reader` and :class:`Writer` classes.
+
+Coverage of PNG formats is fairly complete;
+all allowable bit depths (1/2/4/8/16/24/32/48/64 bits per pixel) and
+colour combinations are supported:
+
+- greyscale (1/2/4/8/16 bit);
+- RGB, RGBA, LA (greyscale with alpha) with 8/16 bits per channel;
+- colour mapped images (1/2/4/8 bit).
+
+Interlaced images,
+which support a progressive display when downloading,
+are supported for both reading and writing.
+
+A number of optional chunks can be specified (when writing)
+and understood (when reading): ``tRNS``, ``bKGD``, ``gAMA``.
+
+The ``sBIT`` chunk can be used to specify precision for
+non-native bit depths.
+
+Requires Python 3.5 or higher.
+Installation is trivial,
+but see the ``README.txt`` file (with the source distribution) for details.
+
+Full use of all features will need some reading of the PNG specification
+http://www.w3.org/TR/2003/REC-PNG-20031110/.
+
+The package also comes with command line utilities.
+
+- ``pripamtopng`` converts
+  `Netpbm <http://netpbm.sourceforge.net/>`_ PAM/PNM files to PNG;
+- ``pripngtopam`` converts PNG to file PAM/PNM.
 
-Requires Python 2.3.  Limited support is available for Python 2.2, but
-not everything works.  Best with Python 2.4 and higher.  Installation is
-trivial, but see the ``README.txt`` file (with the source distribution)
-for details.
+There are a few more for simple PNG manipulations.
 
-This file can also be used as a command-line utility to convert
-`Netpbm <http://netpbm.sourceforge.net/>`_ PNM files to PNG, and the
-reverse conversion from PNG to PNM. The interface is similar to that
-of the ``pnmtopng`` program from Netpbm.  Type ``python png.py --help``
-at the shell prompt for usage and a list of options.
+Spelling and Terminology
+------------------------
 
-A note on spelling and terminology
-----------------------------------
+Generally British English spelling is used in the documentation.
+So that's "greyscale" and "colour".
+This not only matches the author's native language,
+it's also used by the PNG specification.
 
-Generally British English spelling is used in the documentation.  So
-that's "greyscale" and "colour".  This not only matches the author's
-native language, it's also used by the PNG specification.
+Colour Models
+-------------
 
 The major colour models supported by PNG (and hence by PyPNG) are:
-greyscale, RGB, greyscale--alpha, RGB--alpha.  These are sometimes
-referred to using the abbreviations: L, RGB, LA, RGBA.  In this case
-each letter abbreviates a single channel: *L* is for Luminance or Luma
-or Lightness which is the channel used in greyscale images; *R*, *G*,
-*B* stand for Red, Green, Blue, the components of a colour image; *A*
-stands for Alpha, the opacity channel (used for transparency effects,
-but higher values are more opaque, so it makes sense to call it 
-opacity).
-
-A note on formats
------------------
-
-When getting pixel data out of this module (reading) and presenting
-data to this module (writing) there are a number of ways the data could
-be represented as a Python value.  Generally this module uses one of
-three formats called "flat row flat pixel", "boxed row flat pixel", and
-"boxed row boxed pixel".  Basically the concern is whether each pixel
-and each row comes in its own little tuple (box), or not.
+
+- greyscale;
+- greyscale--alpha;
+- RGB;
+- RGB--alpha.
+
+Also referred to using the abbreviations: L, LA, RGB, RGBA.
+Each letter codes a single channel:
+*L* is for Luminance or Luma or Lightness (greyscale images);
+*A* stands for Alpha, the opacity channel
+(used for transparency effects, but higher values are more opaque,
+so it makes sense to call it opacity);
+*R*, *G*, *B* stand for Red, Green, Blue (colour image).
+
+Lists, arrays, sequences, and so on
+-----------------------------------
+
+When getting pixel data out of this module (reading) and
+presenting data to this module (writing) there are
+a number of ways the data could be represented as a Python value.
+
+The preferred format is a sequence of *rows*,
+which each row being a sequence of *values*.
+In this format, the values are in pixel order,
+with all the values from all the pixels in a row
+being concatenated into a single sequence for that row.
 
 Consider an image that is 3 pixels wide by 2 pixels high, and each pixel
 has RGB components:
 
-Boxed row flat pixel::
+Sequence of rows::
 
   list([R,G,B, R,G,B, R,G,B],
        [R,G,B, R,G,B, R,G,B])
 
-Each row appears as its own list, but the pixels are flattened so
-that three values for one pixel simply follow the three values for
-the previous pixel.  This is the most common format used, because it
-provides a good compromise between space and convenience.  PyPNG regards
-itself as at liberty to replace any sequence type with any sufficiently
-compatible other sequence type; in practice each row is an array (from
-the array module), and the outer list is sometimes an iterator rather
-than an explicit list (so that streaming is possible).
+Each row appears as its own list,
+but the pixels are flattened so that three values for one pixel
+simply follow the three values for the previous pixel.
 
-Flat row flat pixel::
+This is the preferred because
+it provides a good compromise between space and convenience.
+PyPNG regards itself as at liberty to replace any sequence type with
+any sufficiently compatible other sequence type;
+in practice each row is an array (``bytearray`` or ``array.array``).
+
+To allow streaming the outer list is sometimes
+an iterator rather than an explicit list.
+
+An alternative format is a single array holding all the values.
+
+Array of values::
 
   [R,G,B, R,G,B, R,G,B,
    R,G,B, R,G,B, R,G,B]
@@ -165,44 +145,39 @@ Flat row flat pixel::
 The entire image is one single giant sequence of colour values.
 Generally an array will be used (to save space), not a list.
 
-Boxed row boxed pixel::
-
-  list([ (R,G,B), (R,G,B), (R,G,B) ],
-       [ (R,G,B), (R,G,B), (R,G,B) ])
-
-Each row appears in its own list, but each pixel also appears in its own
-tuple.  A serious memory burn in Python.
-
-In all cases the top row comes first, and for each row the pixels are
-ordered from left-to-right.  Within a pixel the values appear in the
-order, R-G-B-A (or L-A for greyscale--alpha).
-
-There is a fourth format, mentioned because it is used internally,
-is close to what lies inside a PNG file itself, and has some support
-from the public API.  This format is called packed.  When packed,
-each row is a sequence of bytes (integers from 0 to 255), just as
-it is before PNG scanline filtering is applied.  When the bit depth
-is 8 this is essentially the same as boxed row flat pixel; when the
-bit depth is less than 8, several pixels are packed into each byte;
-when the bit depth is 16 (the only value more than 8 that is supported
-by the PNG image format) each pixel value is decomposed into 2 bytes
-(and `packed` is a misnomer).  This format is used by the
-:meth:`Writer.write_packed` method.  It isn't usually a convenient
-format, but may be just right if the source data for the PNG image
-comes from something that uses a similar format (for example, 1-bit
-BMPs, or another PNG file).
-
-And now, my famous members
---------------------------
+The top row comes first,
+and within each row the pixels are ordered from left-to-right.
+Within a pixel the values appear in the order R-G-B-A
+(or L-A for greyscale--alpha).
+
+There is another format, which should only be used with caution.
+It is mentioned because it is used internally,
+is close to what lies inside a PNG file itself,
+and has some support from the public API.
+This format is called *packed*.
+When packed, each row is a sequence of bytes (integers from 0 to 255),
+just as it is before PNG scanline filtering is applied.
+When the bit depth is 8 this is the same as a sequence of rows;
+when the bit depth is less than 8 (1, 2 and 4),
+several pixels are packed into each byte;
+when the bit depth is 16 each pixel value is decomposed into 2 bytes
+(and `packed` is a misnomer).
+This format is used by the :meth:`Writer.write_packed` method.
+It isn't usually a convenient format,
+but may be just right if the source data for
+the PNG image comes from something that uses a similar format
+(for example, 1-bit BMPs, or another PNG file).
 """
 
-__version__ = "0.0.18"
+__version__ = "0.0.20"
 
+import collections
+import io   # For io.BytesIO
 import itertools
 import math
-import re
 # http://www.python.org/doc/2.4.4/lib/module-operator.html
 import operator
+import re
 import struct
 import sys
 # http://www.python.org/doc/2.4.4/lib/module-warnings.html
@@ -210,17 +185,6 @@ import warnings
 import zlib
 
 from array import array
-from functools import reduce
-
-try:
-    # `cpngfilters` is a Cython module: it must be compiled by
-    # Cython for this import to work.
-    # If this import does work, then it overrides pure-python
-    # filtering functions defined later in this file (see `class
-    # pngfilters`).
-    import cpngfilters as pngfilters
-except ImportError:
-    pass
 
 
 __all__ = ['Image', 'Reader', 'Writer', 'write_chunks', 'from_array']
@@ -228,60 +192,53 @@ __all__ = ['Image', 'Reader', 'Writer', 'write_chunks', 'from_array']
 
 # The PNG signature.
 # http://www.w3.org/TR/PNG/#5PNG-file-signature
-_signature = struct.pack('8B', 137, 80, 78, 71, 13, 10, 26, 10)
+signature = struct.pack('8B', 137, 80, 78, 71, 13, 10, 26, 10)
+
+# The xstart, ystart, xstep, ystep for the Adam7 interlace passes.
+adam7 = ((0, 0, 8, 8),
+         (4, 0, 8, 8),
+         (0, 4, 4, 8),
+         (2, 0, 4, 4),
+         (0, 2, 2, 4),
+         (1, 0, 2, 2),
+         (0, 1, 1, 2))
+
+
+def adam7_generate(width, height):
+    """
+    Generate the coordinates for the reduced scanlines
+    of an Adam7 interlaced image
+    of size `width` by `height` pixels.
+
+    Yields a generator for each pass,
+    and each pass generator yields a series of (x, y, xstep) triples,
+    each one identifying a reduced scanline consisting of
+    pixels starting at (x, y) and taking every xstep pixel to the right.
+    """
+
+    for xstart, ystart, xstep, ystep in adam7:
+        if xstart >= width:
+            continue
+        yield ((xstart, y, xstep) for y in range(ystart, height, ystep))
+
+
+# Models the 'pHYs' chunk (used by the Reader)
+Resolution = collections.namedtuple('_Resolution', 'x y unit_is_meter')
 
-_adam7 = ((0, 0, 8, 8),
-          (4, 0, 8, 8),
-          (0, 4, 4, 8),
-          (2, 0, 4, 4),
-          (0, 2, 2, 4),
-          (1, 0, 2, 2),
-          (0, 1, 1, 2))
 
 def group(s, n):
-    # See http://www.python.org/doc/2.6/library/functions.html#zip
-    return list(zip(*[iter(s)]*n))
+    return list(zip(* [iter(s)] * n))
+
 
 def isarray(x):
     return isinstance(x, array)
 
-def tostring(row):
-    return row.tostring()
-
-def interleave_planes(ipixels, apixels, ipsize, apsize):
-    """
-    Interleave (colour) planes, e.g. RGB + A = RGBA.
-
-    Return an array of pixels consisting of the `ipsize` elements of
-    data from each pixel in `ipixels` followed by the `apsize` elements
-    of data from each pixel in `apixels`.  Conventionally `ipixels`
-    and `apixels` are byte arrays so the sizes are bytes, but it
-    actually works with any arrays of the same type.  The returned
-    array is the same type as the input arrays which should be the
-    same type as each other.
-    """
-
-    itotal = len(ipixels)
-    atotal = len(apixels)
-    newtotal = itotal + atotal
-    newpsize = ipsize + apsize
-    # Set up the output buffer
-    # See http://www.python.org/doc/2.4.4/lib/module-array.html#l2h-1356
-    out = array(ipixels.typecode)
-    # It's annoying that there is no cheap way to set the array size :-(
-    out.extend(ipixels)
-    out.extend(apixels)
-    # Interleave in the pixel data
-    for i in range(ipsize):
-        out[i:newtotal:newpsize] = ipixels[i:itotal:ipsize]
-    for i in range(apsize):
-        out[i+ipsize:newtotal:newpsize] = apixels[i:atotal:apsize]
-    return out
 
 def check_palette(palette):
-    """Check a palette argument (to the :class:`Writer` class)
-    for validity.  Returns the palette as a list if okay; raises an
-    exception otherwise.
+    """
+    Check a palette argument (to the :class:`Writer` class) for validity.
+    Returns the palette as a list if okay;
+    raises an exception otherwise.
     """
 
     # None is the default and is allowed.
@@ -290,25 +247,30 @@ def check_palette(palette):
 
     p = list(palette)
     if not (0 < len(p) <= 256):
-        raise ValueError("a palette must have between 1 and 256 entries")
+        raise ProtocolError(
+            "a palette must have between 1 and 256 entries,"
+            " see https://www.w3.org/TR/PNG/#11PLTE")
     seen_triple = False
-    for i,t in enumerate(p):
-        if len(t) not in (3,4):
-            raise ValueError(
-              "palette entry %d: entries must be 3- or 4-tuples." % i)
+    for i, t in enumerate(p):
+        if len(t) not in (3, 4):
+            raise ProtocolError(
+                "palette entry %d: entries must be 3- or 4-tuples." % i)
         if len(t) == 3:
             seen_triple = True
         if seen_triple and len(t) == 4:
-            raise ValueError(
-              "palette entry %d: all 4-tuples must precede all 3-tuples" % i)
+            raise ProtocolError(
+                "palette entry %d: all 4-tuples must precede all 3-tuples" % i)
         for x in t:
             if int(x) != x or not(0 <= x <= 255):
-                raise ValueError(
-                  "palette entry %d: values must be integer: 0 <= x <= 255" % i)
+                raise ProtocolError(
+                    "palette entry %d: "
+                    "values must be integer: 0 <= x <= 255" % i)
     return p
 
+
 def check_sizes(size, width, height):
-    """Check that these arguments, in supplied, are consistent.
+    """
+    Check that these arguments, if supplied, are consistent.
     Return a (width, height) pair.
     """
 
@@ -316,22 +278,25 @@ def check_sizes(size, width, height):
         return width, height
 
     if len(size) != 2:
-        raise ValueError(
-          "size argument should be a pair (width, height)")
+        raise ProtocolError(
+            "size argument should be a pair (width, height)")
     if width is not None and width != size[0]:
-        raise ValueError(
-          "size[0] (%r) and width (%r) should match when both are used."
+        raise ProtocolError(
+            "size[0] (%r) and width (%r) should match when both are used."
             % (size[0], width))
     if height is not None and height != size[1]:
-        raise ValueError(
-          "size[1] (%r) and height (%r) should match when both are used."
+        raise ProtocolError(
+            "size[1] (%r) and height (%r) should match when both are used."
             % (size[1], height))
     return size
 
+
 def check_color(c, greyscale, which):
-    """Checks that a colour argument for transparent or
-    background options is the right form.  Returns the colour
-    (which, if it's a bar integer, is "corrected" to a 1-tuple).
+    """
+    Checks that a colour argument for transparent or background options
+    is the right form.
+    Returns the colour
+    (which, if it's a bare integer, is "corrected" to a 1-tuple).
     """
 
     if c is None:
@@ -342,33 +307,48 @@ def check_color(c, greyscale, which):
         except TypeError:
             c = (c,)
         if len(c) != 1:
-            raise ValueError("%s for greyscale must be 1-tuple" %
-                which)
-        if not isinteger(c[0]):
-            raise ValueError(
+            raise ProtocolError("%s for greyscale must be 1-tuple" % which)
+        if not is_natural(c[0]):
+            raise ProtocolError(
                 "%s colour for greyscale must be integer" % which)
     else:
         if not (len(c) == 3 and
-                isinteger(c[0]) and
-                isinteger(c[1]) and
-                isinteger(c[2])):
-            raise ValueError(
+                is_natural(c[0]) and
+                is_natural(c[1]) and
+                is_natural(c[2])):
+            raise ProtocolError(
                 "%s colour must be a triple of integers" % which)
     return c
 
+
 class Error(Exception):
     def __str__(self):
         return self.__class__.__name__ + ': ' + ' '.join(self.args)
 
+
 class FormatError(Error):
-    """Problem with input file format.  In other words, PNG file does
-    not conform to the specification in some way and is invalid.
+    """
+    Problem with input file format.
+    In other words, PNG file does not conform to
+    the specification in some way and is invalid.
+    """
+
+
+class ProtocolError(Error):
+    """
+    Problem with the way the programming interface has been used,
+    or the data presented to it.
     """
 
+
 class ChunkError(FormatError):
     pass
 
 
+class Default:
+    """The default for the greyscale paramter."""
+
+
 class Writer:
     """
     PNG encoder in pure Python.
@@ -376,7 +356,7 @@ class Writer:
 
     def __init__(self, width=None, height=None,
                  size=None,
-                 greyscale=False,
+                 greyscale=Default,
                  alpha=False,
                  bitdepth=8,
                  palette=None,
@@ -385,14 +365,13 @@ class Writer:
                  gamma=None,
                  compression=None,
                  interlace=False,
-                 bytes_per_sample=None, # deprecated
                  planes=None,
                  colormap=None,
                  maxval=None,
                  chunk_limit=2**20,
-                 x_pixels_per_unit = None,
-                 y_pixels_per_unit = None,
-                 unit_is_meter = False):
+                 x_pixels_per_unit=None,
+                 y_pixels_per_unit=None,
+                 unit_is_meter=False):
         """
         Create a PNG encoder object.
 
@@ -403,11 +382,11 @@ class Writer:
         size
           Image size (w,h) in pixels, as single argument.
         greyscale
-          Input data is greyscale, not RGB.
+          Pixels are greyscale, not RGB.
         alpha
           Input data has alpha channel (RGBA or LA).
         bitdepth
-          Bit depth: from 1 to 16.
+          Bit depth: from 1 to 16 (for each channel).
         palette
           Create a palette for a colour mapped image (colour type 3).
         transparent
@@ -436,82 +415,100 @@ class Writer:
 
         The image size (in pixels) can be specified either by using the
         `width` and `height` arguments, or with the single `size`
-        argument.  If `size` is used it should be a pair (*width*,
-        *height*).
+        argument.
+        If `size` is used it should be a pair (*width*, *height*).
 
-        `greyscale` and `alpha` are booleans that specify whether
-        an image is greyscale (or colour), and whether it has an
-        alpha channel (or not).
+        The `greyscale` argument indicates whether input pixels
+        are greyscale (when true), or colour (when false).
+        The default is true unless `palette=` is used.
+
+        The `alpha` argument (a boolean) specifies
+        whether input pixels have an alpha channel (or not).
 
         `bitdepth` specifies the bit depth of the source pixel values.
-        Each source pixel value must be an integer between 0 and
-        ``2**bitdepth-1``.  For example, 8-bit images have values
-        between 0 and 255.  PNG only stores images with bit depths of
-        1,2,4,8, or 16.  When `bitdepth` is not one of these values,
-        the next highest valid bit depth is selected, and an ``sBIT``
-        (significant bits) chunk is generated that specifies the
-        original precision of the source image.  In this case the
-        supplied pixel values will be rescaled to fit the range of
-        the selected bit depth.
-
-        The details of which bit depth / colour model combinations the
-        PNG file format supports directly, are somewhat arcane
-        (refer to the PNG specification for full details).  Briefly:
-        "small" bit depths (1,2,4) are only allowed with greyscale and
-        colour mapped images; colour mapped images cannot have bit depth
-        16.
-
-        For colour mapped images (in other words, when the `palette`
-        argument is specified) the `bitdepth` argument must match one of
-        the valid PNG bit depths: 1, 2, 4, or 8.  (It is valid to have a
-        PNG image with a palette and an ``sBIT`` chunk, but the meaning
-        is slightly different; it would be awkward to press the
-        `bitdepth` argument into service for this.)
-
-        The `palette` option, when specified, causes a colour
-        mapped image to be created: the PNG colour type is set to 3;
-        `greyscale` must not be set; `alpha` must not be set;
-        `transparent` must not be set; the bit depth must be 1,2,4,
-        or 8.  When a colour mapped image is created, the pixel values
-        are palette indexes and the `bitdepth` argument specifies the
-        size of these indexes (not the size of the colour values in
-        the palette).
+        Each channel may have a different bit depth.
+        Each source pixel must have values that are
+        an integer between 0 and ``2**bitdepth-1``, where
+        `bitdepth` is the bit depth for the corresponding channel.
+        For example, 8-bit images have values between 0 and 255.
+        PNG only stores images with bit depths of
+        1,2,4,8, or 16 (the same for all channels).
+        When `bitdepth` is not one of these values or where
+        channels have different bit depths,
+        the next highest valid bit depth is selected,
+        and an ``sBIT`` (significant bits) chunk is generated
+        that specifies the original precision of the source image.
+        In this case the supplied pixel values will be rescaled to
+        fit the range of the selected bit depth.
+
+        The PNG file format supports many bit depth / colour model
+        combinations, but not all.
+        The details are somewhat arcane
+        (refer to the PNG specification for full details).
+        Briefly:
+        Bit depths < 8 (1,2,4) are only allowed with greyscale and
+        colour mapped images;
+        colour mapped images cannot have bit depth 16.
+
+        For colour mapped images
+        (in other words, when the `palette` argument is specified)
+        the `bitdepth` argument must match one of
+        the valid PNG bit depths: 1, 2, 4, or 8.
+        (It is valid to have a PNG image with a palette and
+        an ``sBIT`` chunk, but the meaning is slightly different;
+        it would be awkward to use the `bitdepth` argument for this.)
+
+        The `palette` option, when specified,
+        causes a colour mapped image to be created:
+        the PNG colour type is set to 3;
+        `greyscale` must not be true; `alpha` must not be true;
+        `transparent` must not be set.
+        The bit depth must be 1,2,4, or 8.
+        When a colour mapped image is created,
+        the pixel values are palette indexes and
+        the `bitdepth` argument specifies the size of these indexes
+        (not the size of the colour values in the palette).
 
         The palette argument value should be a sequence of 3- or
-        4-tuples.  3-tuples specify RGB palette entries; 4-tuples
-        specify RGBA palette entries.  If both 4-tuples and 3-tuples
-        appear in the sequence then all the 4-tuples must come
-        before all the 3-tuples.  A ``PLTE`` chunk is created; if there
-        are 4-tuples then a ``tRNS`` chunk is created as well.  The
-        ``PLTE`` chunk will contain all the RGB triples in the same
-        sequence; the ``tRNS`` chunk will contain the alpha channel for
-        all the 4-tuples, in the same sequence.  Palette entries
-        are always 8-bit.
-
-        If specified, the `transparent` and `background` parameters must
-        be a tuple with three integer values for red, green, blue, or
-        a simple integer (or singleton tuple) for a greyscale image.
+        4-tuples.
+        3-tuples specify RGB palette entries;
+        4-tuples specify RGBA palette entries.
+        All the 4-tuples (if present) must come before all the 3-tuples.
+        A ``PLTE`` chunk is created;
+        if there are 4-tuples then a ``tRNS`` chunk is created as well.
+        The ``PLTE`` chunk will contain all the RGB triples in the same
+        sequence;
+        the ``tRNS`` chunk will contain the alpha channel for
+        all the 4-tuples, in the same sequence.
+        Palette entries are always 8-bit.
+
+        If specified, the `transparent` and `background` parameters must be
+        a tuple with one element for each channel in the image.
+        Either a 3-tuple of integer (RGB) values for a colour image, or
+        a 1-tuple of a single integer for a greyscale image.
 
         If specified, the `gamma` parameter must be a positive number
-        (generally, a `float`).  A ``gAMA`` chunk will be created.
+        (generally, a `float`).
+        A ``gAMA`` chunk will be created.
         Note that this will not change the values of the pixels as
-        they appear in the PNG file, they are assumed to have already
+        they appear in the PNG file,
+        they are assumed to have already
         been converted appropriately for the gamma specified.
 
         The `compression` argument specifies the compression level to
-        be used by the ``zlib`` module.  Values from 1 to 9 specify
-        compression, with 9 being "more compressed" (usually smaller
-        and slower, but it doesn't always work out that way).  0 means
-        no compression.  -1 and ``None`` both mean that the default
-        level of compession will be picked by the ``zlib`` module
-        (which is generally acceptable).
+        be used by the ``zlib`` module.
+        Values from 1 to 9 (highest) specify compression.
+        0 means no compression.
+        -1 and ``None`` both mean that the ``zlib`` module uses
+        the default level of compession (which is generally acceptable).
 
         If `interlace` is true then an interlaced image is created
-        (using PNG's so far only interace method, *Adam7*).  This does
-        not affect how the pixels should be presented to the encoder,
+        (using PNG's so far only interace method, *Adam7*).
+        This does not affect how the pixels should be passed in,
         rather it changes how they are arranged into the PNG file.
-        On slow connexions interlaced images can be partially decoded
-        by the browser to give a rough view of the image that is
+        On slow connexions interlaced images can be
+        partially decoded by the browser to give
+        a rough view of the image that is
         successively refined as more image data appears.
 
         .. note ::
@@ -520,8 +517,9 @@ class Writer:
           to be processed in working memory.
 
         `chunk_limit` is used to limit the amount of memory used whilst
-        compressing the image.  In order to avoid using large amounts of
-        memory, multiple ``IDAT`` chunks may be created.
+        compressing the image.
+        In order to avoid using large amounts of memory,
+        multiple ``IDAT`` chunks may be created.
         """
 
         # At the moment the `planes` argument is ignored;
@@ -533,85 +531,75 @@ class Writer:
         width, height = check_sizes(size, width, height)
         del size
 
+        if not is_natural(width) or not is_natural(height):
+            raise ProtocolError("width and height must be integers")
         if width <= 0 or height <= 0:
-            raise ValueError("width and height must be greater than zero")
-        if not isinteger(width) or not isinteger(height):
-            raise ValueError("width and height must be integers")
+            raise ProtocolError("width and height must be greater than zero")
         # http://www.w3.org/TR/PNG/#7Integers-and-byte-order
-        if width > 2**32-1 or height > 2**32-1:
-            raise ValueError("width and height cannot exceed 2**32-1")
+        if width > 2 ** 31 - 1 or height > 2 ** 31 - 1:
+            raise ProtocolError("width and height cannot exceed 2**31-1")
 
         if alpha and transparent is not None:
-            raise ValueError(
+            raise ProtocolError(
                 "transparent colour not allowed with alpha channel")
 
-        if bytes_per_sample is not None:
-            warnings.warn('please use bitdepth instead of bytes_per_sample',
-                          DeprecationWarning)
-            if bytes_per_sample not in (0.125, 0.25, 0.5, 1, 2):
-                raise ValueError(
-                    "bytes per sample must be .125, .25, .5, 1, or 2")
-            bitdepth = int(8*bytes_per_sample)
-        del bytes_per_sample
-        if not isinteger(bitdepth) or bitdepth < 1 or 16 < bitdepth:
-            raise ValueError("bitdepth (%r) must be a positive integer <= 16" %
-              bitdepth)
-
-        self.rescale = None
+        # bitdepth is either single integer, or tuple of integers.
+        # Convert to tuple.
+        try:
+            len(bitdepth)
+        except TypeError:
+            bitdepth = (bitdepth, )
+        for b in bitdepth:
+            valid = is_natural(b) and 1 <= b <= 16
+            if not valid:
+                raise ProtocolError(
+                    "each bitdepth %r must be a positive integer <= 16" %
+                    (bitdepth,))
+
+        # Calculate channels, and
+        # expand bitdepth to be one element per channel.
         palette = check_palette(palette)
-        if palette:
-            if bitdepth not in (1,2,4,8):
-                raise ValueError("with palette, bitdepth must be 1, 2, 4, or 8")
-            if transparent is not None:
-                raise ValueError("transparent and palette not compatible")
-            if alpha:
-                raise ValueError("alpha and palette not compatible")
-            if greyscale:
-                raise ValueError("greyscale and palette not compatible")
+        alpha = bool(alpha)
+        colormap = bool(palette)
+        if greyscale is Default and palette:
+            greyscale = False
+        greyscale = bool(greyscale)
+        if colormap:
+            color_planes = 1
+            planes = 1
         else:
-            # No palette, check for sBIT chunk generation.
-            if alpha or not greyscale:
-                if bitdepth not in (8,16):
-                    targetbitdepth = (8,16)[bitdepth > 8]
-                    self.rescale = (bitdepth, targetbitdepth)
-                    bitdepth = targetbitdepth
-                    del targetbitdepth
-            else:
-                assert greyscale
-                assert not alpha
-                if bitdepth not in (1,2,4,8,16):
-                    if bitdepth > 8:
-                        targetbitdepth = 16
-                    elif bitdepth == 3:
-                        targetbitdepth = 4
-                    else:
-                        assert bitdepth in (5,6,7)
-                        targetbitdepth = 8
-                    self.rescale = (bitdepth, targetbitdepth)
-                    bitdepth = targetbitdepth
-                    del targetbitdepth
-
-        if bitdepth < 8 and (alpha or not greyscale and not palette):
-            raise ValueError(
-              "bitdepth < 8 only permitted with greyscale or palette")
-        if bitdepth > 8 and palette:
-            raise ValueError(
-                "bit depth must be 8 or less for images with palette")
+            color_planes = (3, 1)[greyscale]
+            planes = color_planes + alpha
+        if len(bitdepth) == 1:
+            bitdepth *= planes
+
+        bitdepth, self.rescale = check_bitdepth_rescale(
+                palette,
+                bitdepth,
+                transparent, alpha, greyscale)
+
+        # These are assertions, because above logic should have
+        # corrected or raised all problematic cases.
+        if bitdepth < 8:
+            assert greyscale or palette
+            assert not alpha
+        if bitdepth > 8:
+            assert not palette
 
         transparent = check_color(transparent, greyscale, 'transparent')
         background = check_color(background, greyscale, 'background')
 
-        # It's important that the true boolean values (greyscale, alpha,
-        # colormap, interlace) are converted to bool because Iverson's
-        # convention is relied upon later on.
+        # It's important that the true boolean values
+        # (greyscale, alpha, colormap, interlace) are converted
+        # to bool because Iverson's convention is relied upon later on.
         self.width = width
         self.height = height
         self.transparent = transparent
         self.background = background
         self.gamma = gamma
-        self.greyscale = bool(greyscale)
-        self.alpha = bool(alpha)
-        self.colormap = bool(palette)
+        self.greyscale = greyscale
+        self.alpha = alpha
+        self.colormap = colormap
         self.bitdepth = int(bitdepth)
         self.compression = compression
         self.chunk_limit = chunk_limit
@@ -621,81 +609,159 @@ class Writer:
         self.y_pixels_per_unit = y_pixels_per_unit
         self.unit_is_meter = bool(unit_is_meter)
 
-        self.color_type = 4*self.alpha + 2*(not greyscale) + 1*self.colormap
-        assert self.color_type in (0,2,3,4,6)
+        self.color_type = (4 * self.alpha +
+                           2 * (not greyscale) +
+                           1 * self.colormap)
+        assert self.color_type in (0, 2, 3, 4, 6)
 
-        self.color_planes = (3,1)[self.greyscale or self.colormap]
-        self.planes = self.color_planes + self.alpha
+        self.color_planes = color_planes
+        self.planes = planes
         # :todo: fix for bitdepth < 8
-        self.psize = (self.bitdepth/8) * self.planes
-
-    def make_palette(self):
-        """Create the byte sequences for a ``PLTE`` and if necessary a
-        ``tRNS`` chunk.  Returned as a pair (*p*, *t*).  *t* will be
-        ``None`` if no ``tRNS`` chunk is necessary.
-        """
-
-        p = array('B')
-        t = array('B')
-
-        for x in self.palette:
-            p.extend(x[0:3])
-            if len(x) > 3:
-                t.append(x[3])
-        p = tostring(p)
-        t = tostring(t)
-        if t:
-            return p,t
-        return p,None
+        self.psize = (self.bitdepth / 8) * self.planes
 
     def write(self, outfile, rows):
-        """Write a PNG image to the output file.  `rows` should be
-        an iterable that yields each row in boxed row flat pixel
-        format.  The rows should be the rows of the original image,
-        so there should be ``self.height`` rows of ``self.width *
-        self.planes`` values.  If `interlace` is specified (when
-        creating the instance), then an interlaced PNG file will
-        be written.  Supply the rows in the normal image order;
+        """
+        Write a PNG image to the output file.
+        `rows` should be an iterable that yields each row
+        (each row is a sequence of values).
+        The rows should be the rows of the original image,
+        so there should be ``self.height`` rows of
+        ``self.width * self.planes`` values.
+        If `interlace` is specified (when creating the instance),
+        then an interlaced PNG file will be written.
+        Supply the rows in the normal image order;
         the interlacing is carried out internally.
 
         .. note ::
 
-          Interlacing will require the entire image to be in working
-          memory.
+          Interlacing requires the entire image to be in working memory.
         """
 
+        # Values per row
+        vpr = self.width * self.planes
+
+        def check_rows(rows):
+            """
+            Yield each row in rows,
+            but check each row first (for correct width).
+            """
+            for i, row in enumerate(rows):
+                try:
+                    wrong_length = len(row) != vpr
+                except TypeError:
+                    # When using an itertools.ichain object or
+                    # other generator not supporting __len__,
+                    # we set this to False to skip the check.
+                    wrong_length = False
+                if wrong_length:
+                    # Note: row numbers start at 0.
+                    raise ProtocolError(
+                        "Expected %d values but got %d values, in row %d" %
+                        (vpr, len(row), i))
+                yield row
+
         if self.interlace:
             fmt = 'BH'[self.bitdepth > 8]
-            a = array(fmt, itertools.chain(*rows))
+            a = array(fmt, itertools.chain(*check_rows(rows)))
             return self.write_array(outfile, a)
 
-        nrows = self.write_passes(outfile, rows)
+        nrows = self.write_passes(outfile, check_rows(rows))
         if nrows != self.height:
-            raise ValueError(
-              "rows supplied (%d) does not match height (%d)" %
-              (nrows, self.height))
+            raise ProtocolError(
+                "rows supplied (%d) does not match height (%d)" %
+                (nrows, self.height))
+        return nrows
 
-    def write_passes(self, outfile, rows, packed=False):
+    def write_passes(self, outfile, rows):
         """
         Write a PNG image to the output file.
 
         Most users are expected to find the :meth:`write` or
         :meth:`write_array` method more convenient.
-        
+
         The rows should be given to this method in the order that
-        they appear in the output file.  For straightlaced images,
-        this is the usual top to bottom ordering, but for interlaced
-        images the rows should have already been interlaced before
+        they appear in the output file.
+        For straightlaced images, this is the usual top to bottom ordering.
+        For interlaced images the rows should have been interlaced before
         passing them to this function.
 
-        `rows` should be an iterable that yields each row.  When
-        `packed` is ``False`` the rows should be in boxed row flat pixel
-        format; when `packed` is ``True`` each row should be a packed
-        sequence of bytes.
+        `rows` should be an iterable that yields each row
+        (each row being a sequence of values).
         """
 
+        # Ensure rows are scaled (to 4-/8-/16-bit),
+        # and packed into bytes.
+
+        if self.rescale:
+            rows = rescale_rows(rows, self.rescale)
+
+        if self.bitdepth < 8:
+            rows = pack_rows(rows, self.bitdepth)
+        elif self.bitdepth == 16:
+            rows = unpack_rows(rows)
+
+        return self.write_packed(outfile, rows)
+
+    def write_packed(self, outfile, rows):
+        """
+        Write PNG file to `outfile`.
+        `rows` should be an iterator that yields each packed row;
+        a packed row being a sequence of packed bytes.
+
+        The rows have a filter byte prefixed and
+        are then compressed into one or more IDAT chunks.
+        They are not processed any further,
+        so if bitdepth is other than 1, 2, 4, 8, 16,
+        the pixel values should have been scaled
+        before passing them to this method.
+
+        This method does work for interlaced images but it is best avoided.
+        For interlaced images, the rows should be
+        presented in the order that they appear in the file.
+        """
+
+        self.write_preamble(outfile)
+
+        # http://www.w3.org/TR/PNG/#11IDAT
+        if self.compression is not None:
+            compressor = zlib.compressobj(self.compression)
+        else:
+            compressor = zlib.compressobj()
+
+        # data accumulates bytes to be compressed for the IDAT chunk;
+        # it's compressed when sufficiently large.
+        data = bytearray()
+
+        # raise i scope out of the for loop. set to -1, because the for loop
+        # sets i to 0 on the first pass
+        i = -1
+        for i, row in enumerate(rows):
+            # Add "None" filter type.
+            # Currently, it's essential that this filter type be used
+            # for every scanline as
+            # we do not mark the first row of a reduced pass image;
+            # that means we could accidentally compute
+            # the wrong filtered scanline if we used
+            # "up", "average", or "paeth" on such a line.
+            data.append(0)
+            data.extend(row)
+            if len(data) > self.chunk_limit:
+                compressed = compressor.compress(data)
+                if len(compressed):
+                    write_chunk(outfile, b'IDAT', compressed)
+                data = bytearray()
+
+        compressed = compressor.compress(bytes(data))
+        flushed = compressor.flush()
+        if len(compressed) or len(flushed):
+            write_chunk(outfile, b'IDAT', compressed + flushed)
+        # http://www.w3.org/TR/PNG/#11IEND
+        write_chunk(outfile, b'IEND')
+        return i + 1
+
+    def write_preamble(self, outfile):
         # http://www.w3.org/TR/PNG/#5PNG-file-signature
-        outfile.write(_signature)
+        outfile.write(signature)
 
         # http://www.w3.org/TR/PNG/#11IHDR
         write_chunk(outfile, b'IHDR',
@@ -707,245 +773,81 @@ class Writer:
         # http://www.w3.org/TR/PNG/#11gAMA
         if self.gamma is not None:
             write_chunk(outfile, b'gAMA',
-                        struct.pack("!L", int(round(self.gamma*1e5))))
+                        struct.pack("!L", int(round(self.gamma * 1e5))))
 
         # See :chunk:order
         # http://www.w3.org/TR/PNG/#11sBIT
         if self.rescale:
-            write_chunk(outfile, b'sBIT',
+            write_chunk(
+                outfile, b'sBIT',
                 struct.pack('%dB' % self.planes,
-                            *[self.rescale[0]]*self.planes))
-        
-        # :chunk:order: Without a palette (PLTE chunk), ordering is
-        # relatively relaxed.  With one, gAMA chunk must precede PLTE
-        # chunk which must precede tRNS and bKGD.
+                            * [s[0] for s in self.rescale]))
+
+        # :chunk:order: Without a palette (PLTE chunk),
+        # ordering is relatively relaxed.
+        # With one, gAMA chunk must precede PLTE chunk
+        # which must precede tRNS and bKGD.
         # See http://www.w3.org/TR/PNG/#5ChunkOrdering
         if self.palette:
-            p,t = self.make_palette()
+            p, t = make_palette_chunks(self.palette)
             write_chunk(outfile, b'PLTE', p)
             if t:
-                # tRNS chunk is optional. Only needed if palette entries
-                # have alpha.
+                # tRNS chunk is optional;
+                # Only needed if palette entries have alpha.
                 write_chunk(outfile, b'tRNS', t)
 
         # http://www.w3.org/TR/PNG/#11tRNS
         if self.transparent is not None:
             if self.greyscale:
-                write_chunk(outfile, b'tRNS',
-                            struct.pack("!1H", *self.transparent))
+                fmt = "!1H"
             else:
-                write_chunk(outfile, b'tRNS',
-                            struct.pack("!3H", *self.transparent))
+                fmt = "!3H"
+            write_chunk(outfile, b'tRNS',
+                        struct.pack(fmt, *self.transparent))
 
         # http://www.w3.org/TR/PNG/#11bKGD
         if self.background is not None:
             if self.greyscale:
-                write_chunk(outfile, b'bKGD',
-                            struct.pack("!1H", *self.background))
+                fmt = "!1H"
             else:
-                write_chunk(outfile, b'bKGD',
-                            struct.pack("!3H", *self.background))
+                fmt = "!3H"
+            write_chunk(outfile, b'bKGD',
+                        struct.pack(fmt, *self.background))
 
         # http://www.w3.org/TR/PNG/#11pHYs
-        if self.x_pixels_per_unit is not None and self.y_pixels_per_unit is not None:
-            tup = (self.x_pixels_per_unit, self.y_pixels_per_unit, int(self.unit_is_meter))
-            write_chunk(outfile, b'pHYs', struct.pack("!LLB",*tup))
-
-        # http://www.w3.org/TR/PNG/#11IDAT
-        if self.compression is not None:
-            compressor = zlib.compressobj(self.compression)
-        else:
-            compressor = zlib.compressobj()
-
-        # Choose an extend function based on the bitdepth.  The extend
-        # function packs/decomposes the pixel values into bytes and
-        # stuffs them onto the data array.
-        data = array('B')
-        if self.bitdepth == 8 or packed:
-            extend = data.extend
-        elif self.bitdepth == 16:
-            # Decompose into bytes
-            def extend(sl):
-                fmt = '!%dH' % len(sl)
-                data.extend(array('B', struct.pack(fmt, *sl)))
-        else:
-            # Pack into bytes
-            assert self.bitdepth < 8
-            # samples per byte
-            spb = int(8/self.bitdepth)
-            def extend(sl):
-                a = array('B', sl)
-                # Adding padding bytes so we can group into a whole
-                # number of spb-tuples.
-                l = float(len(a))
-                extra = math.ceil(l / float(spb))*spb - l
-                a.extend([0]*int(extra))
-                # Pack into bytes
-                l = group(a, spb)
-                l = [reduce(lambda x,y:
-                                           (x << self.bitdepth) + y, e) for e in l]
-                data.extend(l)
-        if self.rescale:
-            oldextend = extend
-            factor = \
-              float(2**self.rescale[1]-1) / float(2**self.rescale[0]-1)
-            def extend(sl):
-                oldextend([int(round(factor*x)) for x in sl])
-
-        # Build the first row, testing mostly to see if we need to
-        # changed the extend function to cope with NumPy integer types
-        # (they cause our ordinary definition of extend to fail, so we
-        # wrap it).  See
-        # http://code.google.com/p/pypng/issues/detail?id=44
-        enumrows = enumerate(rows)
-        del rows
-
-        # First row's filter type.
-        data.append(0)
-        # :todo: Certain exceptions in the call to ``.next()`` or the
-        # following try would indicate no row data supplied.
-        # Should catch.
-        i,row = next(enumrows)
-        try:
-            # If this fails...
-            extend(row)
-        except:
-            # ... try a version that converts the values to int first.
-            # Not only does this work for the (slightly broken) NumPy
-            # types, there are probably lots of other, unknown, "nearly"
-            # int types it works for.
-            def wrapmapint(f):
-                return lambda sl: f([int(x) for x in sl])
-            extend = wrapmapint(extend)
-            del wrapmapint
-            extend(row)
-
-        for i,row in enumrows:
-            # Add "None" filter type.  Currently, it's essential that
-            # this filter type be used for every scanline as we do not
-            # mark the first row of a reduced pass image; that means we
-            # could accidentally compute the wrong filtered scanline if
-            # we used "up", "average", or "paeth" on such a line.
-            data.append(0)
-            extend(row)
-            if len(data) > self.chunk_limit:
-                compressed = compressor.compress(tostring(data))
-                if len(compressed):
-                    write_chunk(outfile, b'IDAT', compressed)
-                # Because of our very witty definition of ``extend``,
-                # above, we must re-use the same ``data`` object.  Hence
-                # we use ``del`` to empty this one, rather than create a
-                # fresh one (which would be my natural FP instinct).
-                del data[:]
-        if len(data):
-            compressed = compressor.compress(tostring(data))
-        else:
-            compressed = b''
-        flushed = compressor.flush()
-        if len(compressed) or len(flushed):
-            write_chunk(outfile, b'IDAT', compressed + flushed)
-        # http://www.w3.org/TR/PNG/#11IEND
-        write_chunk(outfile, b'IEND')
-        return i+1
+        if (self.x_pixels_per_unit is not None and
+                self.y_pixels_per_unit is not None):
+            tup = (self.x_pixels_per_unit,
+                   self.y_pixels_per_unit,
+                   int(self.unit_is_meter))
+            write_chunk(outfile, b'pHYs', struct.pack("!LLB", *tup))
 
     def write_array(self, outfile, pixels):
         """
-        Write an array in flat row flat pixel format as a PNG file on
-        the output file.  See also :meth:`write` method.
+        Write an array that holds all the image values
+        as a PNG file on the output file.
+        See also :meth:`write` method.
         """
 
         if self.interlace:
-            self.write_passes(outfile, self.array_scanlines_interlace(pixels))
-        else:
-            self.write_passes(outfile, self.array_scanlines(pixels))
-
-    def write_packed(self, outfile, rows):
-        """
-        Write PNG file to `outfile`.  The pixel data comes from `rows`
-        which should be in boxed row packed format.  Each row should be
-        a sequence of packed bytes.
-
-        Technically, this method does work for interlaced images but it
-        is best avoided.  For interlaced images, the rows should be
-        presented in the order that they appear in the file.
-
-        This method should not be used when the source image bit depth
-        is not one naturally supported by PNG; the bit depth should be
-        1, 2, 4, 8, or 16.
-        """
-
-        if self.rescale:
-            raise Error("write_packed method not suitable for bit depth %d" %
-              self.rescale[0])
-        return self.write_passes(outfile, rows, packed=True)
-
-    def convert_pnm(self, infile, outfile):
-        """
-        Convert a PNM file containing raw pixel data into a PNG file
-        with the parameters set in the writer object.  Works for
-        (binary) PGM, PPM, and PAM formats.
-        """
-
-        if self.interlace:
-            pixels = array('B')
-            pixels.fromfile(infile,
-                            (self.bitdepth//8) * self.color_planes *
-                            self.width * self.height)
-            self.write_passes(outfile, self.array_scanlines_interlace(pixels))
-        else:
-            self.write_passes(outfile, self.file_scanlines(infile))
-
-    def convert_ppm_and_pgm(self, ppmfile, pgmfile, outfile):
-        """
-        Convert a PPM and PGM file containing raw pixel data into a
-        PNG outfile with the parameters set in the writer object.
-        """
-        pixels = array('B')
-        pixels.fromfile(ppmfile,
-                        (self.bitdepth//8) * self.color_planes *
-                        self.width * self.height)
-        apixels = array('B')
-        apixels.fromfile(pgmfile,
-                         (self.bitdepth//8) *
-                         self.width * self.height)
-        pixels = interleave_planes(pixels, apixels,
-                                   (self.bitdepth//8) * self.color_planes,
-                                   (self.bitdepth//8))
-        if self.interlace:
-            self.write_passes(outfile, self.array_scanlines_interlace(pixels))
+            if type(pixels) != array:
+                # Coerce to array type
+                fmt = 'BH'[self.bitdepth > 8]
+                pixels = array(fmt, pixels)
+            return self.write_passes(
+                outfile,
+                self.array_scanlines_interlace(pixels)
+            )
         else:
-            self.write_passes(outfile, self.array_scanlines(pixels))
-
-    def file_scanlines(self, infile):
-        """
-        Generates boxed rows in flat pixel format, from the input file
-        `infile`.  It assumes that the input file is in a "Netpbm-like"
-        binary format, and is positioned at the beginning of the first
-        pixel.  The number of pixels to read is taken from the image
-        dimensions (`width`, `height`, `planes`) and the number of bytes
-        per value is implied by the image `bitdepth`.
-        """
-
-        # Values per row
-        vpr = self.width * self.planes
-        row_bytes = vpr
-        if self.bitdepth > 8:
-            assert self.bitdepth == 16
-            row_bytes *= 2
-            fmt = '>%dH' % vpr
-            def line():
-                return array('H', struct.unpack(fmt, infile.read(row_bytes)))
-        else:
-            def line():
-                scanline = array('B', infile.read(row_bytes))
-                return scanline
-        for y in range(self.height):
-            yield line()
+            return self.write_passes(
+                outfile,
+                self.array_scanlines(pixels)
+            )
 
     def array_scanlines(self, pixels):
         """
-        Generates boxed rows (flat pixels) from flat rows (flat pixels)
-        in an array.
+        Generates rows (each a sequence of values) from
+        a single array of values.
         """
 
         # Values per row
@@ -958,10 +860,10 @@ class Writer:
 
     def array_scanlines_interlace(self, pixels):
         """
-        Generator for interlaced scanlines from an array.  `pixels` is
-        the full source image in flat row flat pixel format.  The
-        generator yields each scanline of the reduced passes in turn, in
-        boxed row flat pixel format.
+        Generator for interlaced scanlines from an array.
+        `pixels` is the full source image as a single array of values.
+        The generator yields each scanline of the reduced passes in turn,
+        each scanline being a sequence of values.
         """
 
         # http://www.w3.org/TR/PNG/#8InterlaceMethods
@@ -969,28 +871,34 @@ class Writer:
         fmt = 'BH'[self.bitdepth > 8]
         # Value per row
         vpr = self.width * self.planes
-        for xstart, ystart, xstep, ystep in _adam7:
-            if xstart >= self.width:
-                continue
-            # Pixels per row (of reduced image)
-            ppr = int(math.ceil((self.width-xstart)/float(xstep)))
-            # number of values in reduced image row.
-            row_len = ppr*self.planes
-            for y in range(ystart, self.height, ystep):
+
+        # Each iteration generates a scanline starting at (x, y)
+        # and consisting of every xstep pixels.
+        for lines in adam7_generate(self.width, self.height):
+            for x, y, xstep in lines:
+                # Pixels per row (of reduced image)
+                ppr = int(math.ceil((self.width - x) / float(xstep)))
+                # Values per row (of reduced image)
+                reduced_row_len = ppr * self.planes
                 if xstep == 1:
+                    # Easy case: line is a simple slice.
                     offset = y * vpr
-                    yield pixels[offset:offset+vpr]
-                else:
-                    row = array(fmt)
-                    # There's no easier way to set the length of an array
-                    row.extend(pixels[0:row_len])
-                    offset = y * vpr + xstart * self.planes
-                    end_offset = (y+1) * vpr
-                    skip = self.planes * xstep
-                    for i in range(self.planes):
-                        row[i::self.planes] = \
-                            pixels[offset+i:end_offset:skip]
-                    yield row
+                    yield pixels[offset: offset + vpr]
+                    continue
+                # We have to step by xstep,
+                # which we can do one plane at a time
+                # using the step in Python slices.
+                row = array(fmt)
+                # There's no easier way to set the length of an array
+                row.extend(pixels[0:reduced_row_len])
+                offset = y * vpr + x * self.planes
+                end_offset = (y + 1) * vpr
+                skip = self.planes * xstep
+                for i in range(self.planes):
+                    row[i::self.planes] = \
+                        pixels[offset + i: end_offset: skip]
+                yield row
+
 
 def write_chunk(outfile, tag, data=b''):
     """
@@ -998,126 +906,190 @@ def write_chunk(outfile, tag, data=b''):
     checksum.
     """
 
+    data = bytes(data)
     # http://www.w3.org/TR/PNG/#5Chunk-layout
     outfile.write(struct.pack("!I", len(data)))
     outfile.write(tag)
     outfile.write(data)
     checksum = zlib.crc32(tag)
     checksum = zlib.crc32(data, checksum)
-    checksum &= 2**32-1
+    checksum &= 2 ** 32 - 1
     outfile.write(struct.pack("!I", checksum))
 
+
 def write_chunks(out, chunks):
     """Create a PNG file by writing out the chunks."""
 
-    out.write(_signature)
+    out.write(signature)
     for chunk in chunks:
         write_chunk(out, *chunk)
 
-def filter_scanline(type, line, fo, prev=None):
-    """Apply a scanline filter to a scanline.  `type` specifies the
-    filter type (0 to 4); `line` specifies the current (unfiltered)
-    scanline as a sequence of bytes; `prev` specifies the previous
-    (unfiltered) scanline as a sequence of bytes. `fo` specifies the
-    filter offset; normally this is size of a pixel in bytes (the number
-    of bytes per sample times the number of channels), but when this is
-    < 1 (for bit depths < 8) then the filter offset is 1.
+
+def rescale_rows(rows, rescale):
+    """
+    Take each row in rows (an iterator) and yield
+    a fresh row with the pixels scaled according to
+    the rescale parameters in the list `rescale`.
+    Each element of `rescale` is a tuple of
+    (source_bitdepth, target_bitdepth),
+    with one element per channel.
     """
 
-    assert 0 <= type < 5
-
-    # The output array.  Which, pathetically, we extend one-byte at a
-    # time (fortunately this is linear).
-    out = array('B', [type])
-
-    def sub():
-        ai = -fo
-        for x in line:
-            if ai >= 0:
-                x = (x - line[ai]) & 0xff
-            out.append(x)
-            ai += 1
-    def up():
-        for i,x in enumerate(line):
-            x = (x - prev[i]) & 0xff
-            out.append(x)
-    def average():
-        ai = -fo
-        for i,x in enumerate(line):
-            if ai >= 0:
-                x = (x - ((line[ai] + prev[i]) >> 1)) & 0xff
-            else:
-                x = (x - (prev[i] >> 1)) & 0xff
-            out.append(x)
-            ai += 1
-    def paeth():
-        # http://www.w3.org/TR/PNG/#9Filter-type-4-Paeth
-        ai = -fo # also used for ci
-        for i,x in enumerate(line):
-            a = 0
-            b = prev[i]
-            c = 0
-
-            if ai >= 0:
-                a = line[ai]
-                c = prev[ai]
-            p = a + b - c
-            pa = abs(p - a)
-            pb = abs(p - b)
-            pc = abs(p - c)
-            if pa <= pb and pa <= pc:
-                Pr = a
-            elif pb <= pc:
-                Pr = b
-            else:
-                Pr = c
-
-            x = (x - Pr) & 0xff
-            out.append(x)
-            ai += 1
-
-    if not prev:
-        # We're on the first line.  Some of the filters can be reduced
-        # to simpler cases which makes handling the line "off the top"
-        # of the image simpler.  "up" becomes "none"; "paeth" becomes
-        # "left" (non-trivial, but true). "average" needs to be handled
-        # specially.
-        if type == 2: # "up"
-            type = 0
-        elif type == 3:
-            prev = [0]*len(line)
-        elif type == 4: # "paeth"
-            type = 1
-    if type == 0:
-        out.extend(line)
-    elif type == 1:
-        sub()
-    elif type == 2:
-        up()
-    elif type == 3:
-        average()
-    else: # type == 4
-        paeth()
-    return out
+    # One factor for each channel
+    fs = [float(2 ** s[1] - 1)/float(2 ** s[0] - 1)
+          for s in rescale]
+
+    # Assume all target_bitdepths are the same
+    target_bitdepths = set(s[1] for s in rescale)
+    assert len(target_bitdepths) == 1
+    (target_bitdepth, ) = target_bitdepths
+    typecode = 'BH'[target_bitdepth > 8]
+
+    # Number of channels
+    n_chans = len(rescale)
+
+    for row in rows:
+        rescaled_row = array(typecode, iter(row))
+        for i in range(n_chans):
+            channel = array(
+                typecode,
+                (int(round(fs[i] * x)) for x in row[i::n_chans]))
+            rescaled_row[i::n_chans] = channel
+        yield rescaled_row
+
+
+def pack_rows(rows, bitdepth):
+    """Yield packed rows that are a byte array.
+    Each byte is packed with the values from several pixels.
+    """
+
+    assert bitdepth < 8
+    assert 8 % bitdepth == 0
+
+    # samples per byte
+    spb = int(8 / bitdepth)
+
+    def make_byte(block):
+        """Take a block of (2, 4, or 8) values,
+        and pack them into a single byte.
+        """
+
+        res = 0
+        for v in block:
+            res = (res << bitdepth) + v
+        return res
+
+    for row in rows:
+        a = bytearray(row)
+        # Adding padding bytes so we can group into a whole
+        # number of spb-tuples.
+        n = float(len(a))
+        extra = math.ceil(n / spb) * spb - n
+        a.extend([0] * int(extra))
+        # Pack into bytes.
+        # Each block is the samples for one byte.
+        blocks = group(a, spb)
+        yield bytearray(make_byte(block) for block in blocks)
+
+
+def unpack_rows(rows):
+    """Unpack each row from being 16-bits per value,
+    to being a sequence of bytes.
+    """
+    for row in rows:
+        fmt = '!%dH' % len(row)
+        yield bytearray(struct.pack(fmt, *row))
+
+
+def make_palette_chunks(palette):
+    """
+    Create the byte sequences for a ``PLTE`` and
+    if necessary a ``tRNS`` chunk.
+    Returned as a pair (*p*, *t*).
+    *t* will be ``None`` if no ``tRNS`` chunk is necessary.
+    """
+
+    p = bytearray()
+    t = bytearray()
+
+    for x in palette:
+        p.extend(x[0:3])
+        if len(x) > 3:
+            t.append(x[3])
+    if t:
+        return p, t
+    return p, None
+
+
+def check_bitdepth_rescale(
+        palette, bitdepth, transparent, alpha, greyscale):
+    """
+    Returns (bitdepth, rescale) pair.
+    """
+
+    if palette:
+        if len(bitdepth) != 1:
+            raise ProtocolError(
+                "with palette, only a single bitdepth may be used")
+        (bitdepth, ) = bitdepth
+        if bitdepth not in (1, 2, 4, 8):
+            raise ProtocolError(
+                "with palette, bitdepth must be 1, 2, 4, or 8")
+        if transparent is not None:
+            raise ProtocolError("transparent and palette not compatible")
+        if alpha:
+            raise ProtocolError("alpha and palette not compatible")
+        if greyscale:
+            raise ProtocolError("greyscale and palette not compatible")
+        return bitdepth, None
+
+    # No palette, check for sBIT chunk generation.
+
+    if greyscale and not alpha:
+        # Single channel, L.
+        (bitdepth,) = bitdepth
+        if bitdepth in (1, 2, 4, 8, 16):
+            return bitdepth, None
+        if bitdepth > 8:
+            targetbitdepth = 16
+        elif bitdepth == 3:
+            targetbitdepth = 4
+        else:
+            assert bitdepth in (5, 6, 7)
+            targetbitdepth = 8
+        return targetbitdepth, [(bitdepth, targetbitdepth)]
+
+    assert alpha or not greyscale
+
+    depth_set = tuple(set(bitdepth))
+    if depth_set in [(8,), (16,)]:
+        # No sBIT required.
+        (bitdepth, ) = depth_set
+        return bitdepth, None
+
+    targetbitdepth = (8, 16)[max(bitdepth) > 8]
+    return targetbitdepth, [(b, targetbitdepth) for b in bitdepth]
 
 
 # Regex for decoding mode string
 RegexModeDecode = re.compile("(LA?|RGBA?);?([0-9]*)", flags=re.IGNORECASE)
 
+
 def from_array(a, mode=None, info={}):
-    """Create a PNG :class:`Image` object from a 2- or 3-dimensional
-    array.  One application of this function is easy PIL-style saving:
+    """
+    Create a PNG :class:`Image` object from a 2-dimensional array.
+    One application of this function is easy PIL-style saving:
     ``png.from_array(pixels, 'L').save('foo.png')``.
 
-    Unless they are specified using the *info* parameter, the PNG's
-    height and width are taken from the array size.  For a 3 dimensional
-    array the first axis is the height; the second axis is the width;
-    and the third axis is the channel number.  Thus an RGB image that is
-    16 pixels high and 8 wide will use an array that is 16x8x3.  For 2
-    dimensional arrays the first axis is the height, but the second axis
-    is ``width*channels``, so an RGB image that is 16 pixels high and 8
-    wide will use a 2-dimensional array that is 16x24 (each row will be
-    8*3 = 24 sample values).
+    Unless they are specified using the *info* parameter,
+    the PNG's height and width are taken from the array size.
+    The first axis is the height; the second axis is the
+    ravelled width and channel index.
+    The array is treated is a sequence of rows,
+    each row being a sequence of values (``width*channels`` in number).
+    So an RGB image that is 16 pixels high and 8 wide will
+    occupy a 2-dimensional array that is 16x24
+    (each row will be 8*3 = 24 sample values).
 
     *mode* is a string that specifies the image colour format in a
     PIL-style mode.  It can be:
@@ -1131,54 +1103,59 @@ def from_array(a, mode=None, info={}):
     ``'RGBA'``
       colour image with alpha (4 channel)
 
-    The mode string can also specify the bit depth (overriding how this
-    function normally derives the bit depth, see below).  Appending
-    ``';16'`` to the mode will cause the PNG to be 16 bits per channel;
+    The mode string can also specify the bit depth
+    (overriding how this function normally derives the bit depth,
+    see below).
+    Appending ``';16'`` to the mode will cause the PNG to be
+    16 bits per channel;
     any decimal from 1 to 16 can be used to specify the bit depth.
 
     When a 2-dimensional array is used *mode* determines how many
     channels the image has, and so allows the width to be derived from
     the second array dimension.
 
-    The array is expected to be a ``numpy`` array, but it can be any
-    suitable Python sequence.  For example, a list of lists can be used:
-    ``png.from_array([[0, 255, 0], [255, 0, 255]], 'L')``.  The exact
-    rules are: ``len(a)`` gives the first dimension, height;
-    ``len(a[0])`` gives the second dimension; ``len(a[0][0])`` gives the
-    third dimension, unless an exception is raised in which case a
-    2-dimensional array is assumed.  It's slightly more complicated than
-    that because an iterator of rows can be used, and it all still
-    works.  Using an iterator allows data to be streamed efficiently.
-
-    The bit depth of the PNG is normally taken from the array element's
-    datatype (but if *mode* specifies a bitdepth then that is used
-    instead).  The array element's datatype is determined in a way which
+    The array is expected to be a ``numpy`` array,
+    but it can be any suitable Python sequence.
+    For example, a list of lists can be used:
+    ``png.from_array([[0, 255, 0], [255, 0, 255]], 'L')``.
+    The exact rules are: ``len(a)`` gives the first dimension, height;
+    ``len(a[0])`` gives the second dimension.
+    It's slightly more complicated than that because
+    an iterator of rows can be used, and it all still works.
+    Using an iterator allows data to be streamed efficiently.
+
+    The bit depth of the PNG is normally taken from
+    the array element's datatype
+    (but if *mode* specifies a bitdepth then that is used instead).
+    The array element's datatype is determined in a way which
     is supposed to work both for ``numpy`` arrays and for Python
-    ``array.array`` objects.  A 1 byte datatype will give a bit depth of
-    8, a 2 byte datatype will give a bit depth of 16.  If the datatype
-    does not have an implicit size, for example it is a plain Python
-    list of lists, as above, then a default of 8 is used.
-
-    The *info* parameter is a dictionary that can be used to specify
-    metadata (in the same style as the arguments to the
-    :class:`png.Writer` class).  For this function the keys that are
-    useful are:
-    
+    ``array.array`` objects.
+    A 1 byte datatype will give a bit depth of 8,
+    a 2 byte datatype will give a bit depth of 16.
+    If the datatype does not have an implicit size,
+    like the above example where it is a plain Python list of lists,
+    then a default of 8 is used.
+
+    The *info* parameter is a dictionary that can
+    be used to specify metadata (in the same style as
+    the arguments to the :class:`png.Writer` class).
+    For this function the keys that are useful are:
+
     height
-      overrides the height derived from the array dimensions and allows
-      *a* to be an iterable.
+      overrides the height derived from the array dimensions and
+      allows *a* to be an iterable.
     width
       overrides the width derived from the array dimensions.
     bitdepth
-      overrides the bit depth derived from the element datatype (but
-      must match *mode* if that also specifies a bit depth).
+      overrides the bit depth derived from the element datatype
+      (but must match *mode* if that also specifies a bit depth).
 
-    Generally anything specified in the
-    *info* dictionary will override any implicit choices that this
-    function would otherwise make, but must match any explicit ones.
+    Generally anything specified in the *info* dictionary will
+    override any implicit choices that this function would otherwise make,
+    but must match any explicit ones.
     For example, if the *info* dictionary has a ``greyscale`` key then
-    this must be true when mode is ``'L'`` or ``'LA'`` and false when
-    mode is ``'RGB'`` or ``'RGBA'``.
+    this must be true when mode is ``'L'`` or ``'LA'`` and
+    false when mode is ``'RGB'`` or ``'RGBA'``.
     """
 
     # We abuse the *info* parameter by modifying it.  Take a copy here.
@@ -1191,47 +1168,46 @@ def from_array(a, mode=None, info={}):
         raise Error("mode string should be 'RGB' or 'L;16' or similar.")
 
     mode, bitdepth = match.groups()
-    alpha = 'A' in mode
     if bitdepth:
         bitdepth = int(bitdepth)
 
     # Colour format.
     if 'greyscale' in info:
         if bool(info['greyscale']) != ('L' in mode):
-            raise Error("info['greyscale'] should match mode.")
+            raise ProtocolError("info['greyscale'] should match mode.")
     info['greyscale'] = 'L' in mode
 
+    alpha = 'A' in mode
     if 'alpha' in info:
         if bool(info['alpha']) != alpha:
-            raise Error("info['alpha'] should match mode.")
+            raise ProtocolError("info['alpha'] should match mode.")
     info['alpha'] = alpha
 
     # Get bitdepth from *mode* if possible.
     if bitdepth:
         if info.get("bitdepth") and bitdepth != info['bitdepth']:
-            raise Error("bitdepth (%d) should match bitdepth of info (%d)." %
-              (bitdepth, info['bitdepth']))
+            raise ProtocolError(
+                "bitdepth (%d) should match bitdepth of info (%d)." %
+                (bitdepth, info['bitdepth']))
         info['bitdepth'] = bitdepth
 
     # Fill in and/or check entries in *info*.
     # Dimensions.
-    if 'size' in info:
-        assert len(info["size"]) == 2
-
-        # Check width, height, size all match where used.
-        for dimension,axis in [('width', 0), ('height', 1)]:
-            if dimension in info:
-                if info[dimension] != info['size'][axis]:
-                    raise Error(
-                      "info[%r] should match info['size'][%r]." %
-                      (dimension, axis))
-        info['width'],info['height'] = info['size']
-
-    if 'height' not in info:
+    width, height = check_sizes(
+        info.get("size"),
+        info.get("width"),
+        info.get("height"))
+    if width:
+        info["width"] = width
+    if height:
+        info["height"] = height
+
+    if "height" not in info:
         try:
             info['height'] = len(a)
         except TypeError:
-            raise Error("len(a) does not work, supply info['height'] instead.")
+            raise ProtocolError(
+                "len(a) does not work, supply info['height'] instead.")
 
     planes = len(mode)
     if 'planes' in info:
@@ -1241,27 +1217,15 @@ def from_array(a, mode=None, info={}):
     # In order to work out whether we the array is 2D or 3D we need its
     # first row, which requires that we take a copy of its iterator.
     # We may also need the first row to derive width and bitdepth.
-    a,t = itertools.tee(a)
+    a, t = itertools.tee(a)
     row = next(t)
     del t
-    try:
-        row[0][0]
-        threed = True
-        testelement = row[0]
-    except (IndexError, TypeError):
-        threed = False
-        testelement = row
+
+    testelement = row
     if 'width' not in info:
-        if threed:
-            width = len(row)
-        else:
-            width = len(row) // planes
+        width = len(row) // planes
         info['width'] = width
 
-    if threed:
-        # Flatten the threed rows
-        a = (itertools.chain.from_iterable(x) for x in a)
-
     if 'bitdepth' not in info:
         try:
             dtype = testelement.dtype
@@ -1271,12 +1235,12 @@ def from_array(a, mode=None, info={}):
                 # Try a Python array.array.
                 bitdepth = 8 * testelement.itemsize
             except AttributeError:
-                # We can't determine it from the array element's
-                # datatype, use a default of 8.
+                # We can't determine it from the array element's datatype,
+                # use a default of 8.
                 bitdepth = 8
         else:
-            # If we got here without exception, we now assume that
-            # the array is a numpy array.
+            # If we got here without exception,
+            # we now assume that the array is a numpy array.
             if dtype.kind == 'b':
                 bitdepth = 1
             else:
@@ -1288,9 +1252,11 @@ def from_array(a, mode=None, info={}):
 
     return Image(a, info)
 
+
 # So that refugee's from PIL feel more at home.  Not documented.
 fromarray = from_array
 
+
 class Image:
     """A PNG image.  You can create an :class:`Image` object from
     an array of pixels by calling :meth:`png.from_array`.  It can be
@@ -1300,85 +1266,70 @@ class Image:
     def __init__(self, rows, info):
         """
         .. note ::
-        
+
           The constructor is not public.  Please do not call it.
         """
-        
+
         self.rows = rows
         self.info = info
 
     def save(self, file):
-        """Save the image to *file*.  If *file* looks like an open file
-        descriptor then it is used, otherwise it is treated as a
-        filename and a fresh file is opened.
-
-        In general, you can only call this method once; after it has
-        been called the first time and the PNG image has been saved, the
-        source data will have been streamed, and cannot be streamed
-        again.
+        """Save the image to the named *file*.
+
+        See `.write()` if you already have an open file object.
+
+        In general, you can only call this method once;
+        after it has been called the first time the PNG image is written,
+        the source data will have been streamed, and
+        cannot be streamed again.
         """
 
         w = Writer(**self.info)
 
-        try:
-            file.write
-            def close(): pass
-        except AttributeError:
-            file = open(file, 'wb')
-            def close(): file.close()
+        with open(file, 'wb') as fd:
+            w.write(fd, self.rows)
 
-        try:
-            w.write(file, self.rows)
-        finally:
-            close()
+    def write(self, file):
+        """Write the image to the open file object.
 
-class _readable:
-    """
-    A simple file-like interface for strings and arrays.
-    """
+        See `.save()` if you have a filename.
+
+        In general, you can only call this method once;
+        after it has been called the first time the PNG image is written,
+        the source data will have been streamed, and
+        cannot be streamed again.
+        """
+
+        w = Writer(**self.info)
+        w.write(file, self.rows)
 
-    def __init__(self, buf):
-        self.buf = buf
-        self.offset = 0
-
-    def read(self, n):
-        r = self.buf[self.offset:self.offset+n]
-        if isarray(r):
-            r = r.tostring()
-        self.offset += n
-        return r
-
-try:
-    str(b'dummy', 'ascii')
-except TypeError:
-    as_str = str
-else:
-    def as_str(x):
-        return str(x, 'ascii')
 
 class Reader:
     """
-    PNG decoder in pure Python.
+    Pure Python PNG decoder in pure Python.
     """
 
-    def __init__(self, _guess=None, **kw):
+    def __init__(self, _guess=None, filename=None, file=None, bytes=None):
         """
-        Create a PNG decoder object.
-
-        The constructor expects exactly one keyword argument. If you
-        supply a positional argument instead, it will guess the input
-        type. You can choose among the following keyword arguments:
+        The constructor expects exactly one keyword argument.
+        If you supply a positional argument instead,
+        it will guess the input type.
+        Choose from the following keyword arguments:
 
         filename
           Name of input file (a PNG file).
         file
           A file-like object (object with a read() method).
         bytes
-          ``array`` or ``string`` with PNG data.
+          ``bytes`` or ``bytearray`` with PNG data.
 
         """
-        if ((_guess is not None and len(kw) != 0) or
-            (_guess is None and len(kw) != 1)):
+        keywords_supplied = (
+            (_guess is not None) +
+            (filename is not None) +
+            (file is not None) +
+            (bytes is not None))
+        if keywords_supplied != 1:
             raise TypeError("Reader() takes exactly 1 argument")
 
         # Will be the first 8 bytes, later on.  See validate_signature.
@@ -1386,80 +1337,70 @@ class Reader:
         self.transparent = None
         # A pair of (len,type) if a chunk has been read but its data and
         # checksum have not (in other words the file position is just
-        # past the 4 bytes that specify the chunk type).  See preamble
-        # method for how this is used.
+        # past the 4 bytes that specify the chunk type).
+        # See preamble method for how this is used.
         self.atchunk = None
 
         if _guess is not None:
             if isarray(_guess):
-                kw["bytes"] = _guess
+                bytes = _guess
             elif isinstance(_guess, str):
-                kw["filename"] = _guess
+                filename = _guess
             elif hasattr(_guess, 'read'):
-                kw["file"] = _guess
-
-        if "filename" in kw:
-            self.file = open(kw["filename"], "rb")
-        elif "file" in kw:
-            self.file = kw["file"]
-        elif "bytes" in kw:
-            self.file = _readable(kw["bytes"])
+                file = _guess
+
+        if bytes is not None:
+            self.file = io.BytesIO(bytes)
+        elif filename is not None:
+            self.file = open(filename, "rb")
+        elif file is not None:
+            self.file = file
         else:
-            raise TypeError("expecting filename, file or bytes array")
+            raise ProtocolError("expecting filename, file or bytes array")
 
-
-    def chunk(self, seek=None, lenient=False):
+    def chunk(self, lenient=False):
         """
-        Read the next PNG chunk from the input file; returns a
-        (*type*, *data*) tuple.  *type* is the chunk's type as a
-        byte string (all PNG chunk types are 4 bytes long).
+        Read the next PNG chunk from the input file;
+        returns a (*type*, *data*) tuple.
+        *type* is the chunk's type as a byte string
+        (all PNG chunk types are 4 bytes long).
         *data* is the chunk's data content, as a byte string.
 
-        If the optional `seek` argument is
-        specified then it will keep reading chunks until it either runs
-        out of file or finds the type specified by the argument.  Note
-        that in general the order of chunks in PNGs is unspecified, so
-        using `seek` can cause you to miss chunks.
-
         If the optional `lenient` argument evaluates to `True`,
         checksum failures will raise warnings rather than exceptions.
         """
 
         self.validate_signature()
 
-        while True:
-            # http://www.w3.org/TR/PNG/#5Chunk-layout
-            if not self.atchunk:
-                self.atchunk = self.chunklentype()
-            length, type = self.atchunk
-            self.atchunk = None
-            data = self.file.read(length)
-            if len(data) != length:
-                raise ChunkError('Chunk %s too short for required %i octets.'
-                  % (type, length))
-            checksum = self.file.read(4)
-            if len(checksum) != 4:
-                raise ChunkError('Chunk %s too short for checksum.' % type)
-            if seek and type != seek:
-                continue
-            verify = zlib.crc32(type)
-            verify = zlib.crc32(data, verify)
-            # Whether the output from zlib.crc32 is signed or not varies
-            # according to hideous implementation details, see
-            # http://bugs.python.org/issue1202 .
-            # We coerce it to be positive here (in a way which works on
-            # Python 2.3 and older).
-            verify &= 2**32 - 1
-            verify = struct.pack('!I', verify)
-            if checksum != verify:
-                (a, ) = struct.unpack('!I', checksum)
-                (b, ) = struct.unpack('!I', verify)
-                message = "Checksum error in %s chunk: 0x%08X != 0x%08X." % (type, a, b)
-                if lenient:
-                    warnings.warn(message, RuntimeWarning)
-                else:
-                    raise ChunkError(message)
-            return type, data
+        # http://www.w3.org/TR/PNG/#5Chunk-layout
+        if not self.atchunk:
+            self.atchunk = self._chunk_len_type()
+        if not self.atchunk:
+            raise ChunkError("No more chunks.")
+        length, type = self.atchunk
+        self.atchunk = None
+
+        data = self.file.read(length)
+        if len(data) != length:
+            raise ChunkError(
+                'Chunk %s too short for required %i octets.'
+                % (type, length))
+        checksum = self.file.read(4)
+        if len(checksum) != 4:
+            raise ChunkError('Chunk %s too short for checksum.' % type)
+        verify = zlib.crc32(type)
+        verify = zlib.crc32(data, verify)
+        verify = struct.pack('!I', verify)
+        if checksum != verify:
+            (a, ) = struct.unpack('!I', checksum)
+            (b, ) = struct.unpack('!I', verify)
+            message = ("Checksum error in %s chunk: 0x%08X != 0x%08X."
+                       % (type.decode('ascii'), a, b))
+            if lenient:
+                warnings.warn(message, RuntimeWarning)
+            else:
+                raise ChunkError(message)
+        return type, data
 
     def chunks(self):
         """Return an iterator that will yield each chunk as a
@@ -1467,38 +1408,40 @@ class Reader:
         """
 
         while True:
-            t,v = self.chunk()
-            yield t,v
+            t, v = self.chunk()
+            yield t, v
             if t == b'IEND':
                 break
 
     def undo_filter(self, filter_type, scanline, previous):
-        """Undo the filter for a scanline.  `scanline` is a sequence of
-        bytes that does not include the initial filter type byte.
-        `previous` is decoded previous scanline (for straightlaced
-        images this is the previous pixel row, but for interlaced
-        images, it is the previous scanline in the reduced image, which
-        in general is not the previous pixel row in the final image).
-        When there is no previous scanline (the first row of a
-        straightlaced image, or the first row in one of the passes in an
-        interlaced image), then this argument should be ``None``.
-
-        The scanline will have the effects of filtering removed, and the
-        result will be returned as a fresh sequence of bytes.
+        """
+        Undo the filter for a scanline.
+        `scanline` is a sequence of bytes that
+        does not include the initial filter type byte.
+        `previous` is decoded previous scanline
+        (for straightlaced images this is the previous pixel row,
+        but for interlaced images, it is
+        the previous scanline in the reduced image,
+        which in general is not the previous pixel row in the final image).
+        When there is no previous scanline
+        (the first row of a straightlaced image,
+        or the first row in one of the passes in an interlaced image),
+        then this argument should be ``None``.
+
+        The scanline will have the effects of filtering removed;
+        the result will be returned as a fresh sequence of bytes.
         """
 
         # :todo: Would it be better to update scanline in place?
-        # Yes, with the Cython extension making the undo_filter fast,
-        # updating scanline inplace makes the code 3 times faster
-        # (reading 50 images of 800x800 went from 40s to 16s)
         result = scanline
 
         if filter_type == 0:
             return result
 
-        if filter_type not in (1,2,3,4):
-            raise FormatError('Invalid PNG Filter Type.'
-              '  See http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters .')
+        if filter_type not in (1, 2, 3, 4):
+            raise FormatError(
+                'Invalid PNG Filter Type.  '
+                'See http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters .')
 
         # Filter unit.  The stride from one pixel to the corresponding
         # byte from the previous pixel.  Normally this is the pixel
@@ -1511,252 +1454,176 @@ class Reader:
         # first line 'up' is the same as 'null', 'paeth' is the same
         # as 'sub', with only 'average' requiring any special case.
         if not previous:
-            previous = array('B', [0]*len(scanline))
-
-        def sub():
-            """Undo sub filter."""
-
-            ai = 0
-            # Loop starts at index fu.  Observe that the initial part
-            # of the result is already filled in correctly with
-            # scanline.
-            for i in range(fu, len(result)):
-                x = scanline[i]
-                a = result[ai]
-                result[i] = (x + a) & 0xff
-                ai += 1
-
-        def up():
-            """Undo up filter."""
-
-            for i in range(len(result)):
-                x = scanline[i]
-                b = previous[i]
-                result[i] = (x + b) & 0xff
-
-        def average():
-            """Undo average filter."""
-
-            ai = -fu
-            for i in range(len(result)):
-                x = scanline[i]
-                if ai < 0:
-                    a = 0
-                else:
-                    a = result[ai]
-                b = previous[i]
-                result[i] = (x + ((a + b) >> 1)) & 0xff
-                ai += 1
-
-        def paeth():
-            """Undo Paeth filter."""
-
-            # Also used for ci.
-            ai = -fu
-            for i in range(len(result)):
-                x = scanline[i]
-                if ai < 0:
-                    a = c = 0
-                else:
-                    a = result[ai]
-                    c = previous[ai]
-                b = previous[i]
-                p = a + b - c
-                pa = abs(p - a)
-                pb = abs(p - b)
-                pc = abs(p - c)
-                if pa <= pb and pa <= pc:
-                    pr = a
-                elif pb <= pc:
-                    pr = b
-                else:
-                    pr = c
-                result[i] = (x + pr) & 0xff
-                ai += 1
+            previous = bytearray([0] * len(scanline))
 
         # Call appropriate filter algorithm.  Note that 0 has already
         # been dealt with.
-        (None,
-         pngfilters.undo_filter_sub,
-         pngfilters.undo_filter_up,
-         pngfilters.undo_filter_average,
-         pngfilters.undo_filter_paeth)[filter_type](fu, scanline, previous, result)
+        fn = (None,
+              undo_filter_sub,
+              undo_filter_up,
+              undo_filter_average,
+              undo_filter_paeth)[filter_type]
+        fn(fu, scanline, previous, result)
         return result
 
-    def deinterlace(self, raw):
+    def _deinterlace(self, raw):
         """
         Read raw pixel data, undo filters, deinterlace, and flatten.
-        Return in flat row flat pixel format.
+        Return a single array of values.
         """
 
         # Values per row (of the target image)
         vpr = self.width * self.planes
 
-        # Make a result array, and make it big enough.  Interleaving
-        # writes to the output array randomly (well, not quite), so the
-        # entire output array must be in memory.
-        fmt = 'BH'[self.bitdepth > 8]
-        a = array(fmt, [0]*vpr*self.height)
+        # Values per image
+        vpi = vpr * self.height
+        # Interleaving writes to the output array randomly
+        # (well, not quite), so the entire output array must be in memory.
+        # Make a result array, and make it big enough.
+        if self.bitdepth > 8:
+            a = array('H', [0] * vpi)
+        else:
+            a = bytearray([0] * vpi)
         source_offset = 0
 
-        for xstart, ystart, xstep, ystep in _adam7:
-            if xstart >= self.width:
-                continue
-            # The previous (reconstructed) scanline.  None at the
-            # beginning of a pass to indicate that there is no previous
-            # line.
+        for lines in adam7_generate(self.width, self.height):
+            # The previous (reconstructed) scanline.
+            # `None` at the beginning of a pass
+            # to indicate that there is no previous line.
             recon = None
-            # Pixels per row (reduced pass image)
-            ppr = int(math.ceil((self.width-xstart)/float(xstep)))
-            # Row size in bytes for this pass.
-            row_size = int(math.ceil(self.psize * ppr))
-            for y in range(ystart, self.height, ystep):
+            for x, y, xstep in lines:
+                # Pixels per row (reduced pass image)
+                ppr = int(math.ceil((self.width - x) / float(xstep)))
+                # Row size in bytes for this pass.
+                row_size = int(math.ceil(self.psize * ppr))
+
                 filter_type = raw[source_offset]
                 source_offset += 1
-                scanline = raw[source_offset:source_offset+row_size]
+                scanline = raw[source_offset: source_offset + row_size]
                 source_offset += row_size
                 recon = self.undo_filter(filter_type, scanline, recon)
                 # Convert so that there is one element per pixel value
-                flat = self.serialtoflat(recon, ppr)
+                flat = self._bytes_to_values(recon, width=ppr)
                 if xstep == 1:
-                    assert xstart == 0
+                    assert x == 0
                     offset = y * vpr
-                    a[offset:offset+vpr] = flat
+                    a[offset: offset + vpr] = flat
                 else:
-                    offset = y * vpr + xstart * self.planes
-                    end_offset = (y+1) * vpr
+                    offset = y * vpr + x * self.planes
+                    end_offset = (y + 1) * vpr
                     skip = self.planes * xstep
                     for i in range(self.planes):
-                        a[offset+i:end_offset:skip] = \
-                            flat[i::self.planes]
+                        a[offset + i: end_offset: skip] = \
+                            flat[i:: self.planes]
+
         return a
 
-    def iterboxed(self, rows):
-        """Iterator that yields each scanline in boxed row flat pixel
-        format.  `rows` should be an iterator that yields the bytes of
-        each row in turn.
+    def _iter_bytes_to_values(self, byte_rows):
+        """
+        Iterator that yields each scanline;
+        each scanline being a sequence of values.
+        `byte_rows` should be an iterator that yields
+        the bytes of each row in turn.
         """
 
-        def asvalues(raw):
-            """Convert a row of raw bytes into a flat row.  Result will
-            be a freshly allocated object, not shared with
-            argument.
-            """
+        for row in byte_rows:
+            yield self._bytes_to_values(row)
 
-            if self.bitdepth == 8:
-                return array('B', raw)
-            if self.bitdepth == 16:
-                raw = tostring(raw)
-                return array('H', struct.unpack('!%dH' % (len(raw)//2), raw))
-            assert self.bitdepth < 8
-            width = self.width
-            # Samples per byte
-            spb = 8//self.bitdepth
-            out = array('B')
-            mask = 2**self.bitdepth - 1
-            shifts = [self.bitdepth * i
-                for i in reversed(list(range(spb)))]
-            for o in raw:
-                out.extend([mask&(o>>i) for i in shifts])
-            return out[:width]
-
-        return map(asvalues, rows)
-
-    def serialtoflat(self, bytes, width=None):
-        """Convert serial format (byte stream) pixel data to flat row
-        flat pixel.
+    def _bytes_to_values(self, bs, width=None):
+        """Convert a packed row of bytes into a row of values.
+        Result will be a freshly allocated object,
+        not shared with the argument.
         """
 
         if self.bitdepth == 8:
-            return bytes
+            return bytearray(bs)
         if self.bitdepth == 16:
-            bytes = tostring(bytes)
             return array('H',
-              struct.unpack('!%dH' % (len(bytes)//2), bytes))
+                         struct.unpack('!%dH' % (len(bs) // 2), bs))
+
         assert self.bitdepth < 8
         if width is None:
             width = self.width
         # Samples per byte
-        spb = 8//self.bitdepth
-        out = array('B')
+        spb = 8 // self.bitdepth
+        out = bytearray()
         mask = 2**self.bitdepth - 1
-        shifts = list(map(self.bitdepth.__mul__, reversed(list(range(spb)))))
-        l = width
-        for o in bytes:
-            out.extend([(mask&(o>>s)) for s in shifts][:l])
-            l -= spb
-            if l <= 0:
-                l = width
-        return out
-
-    def iterstraight(self, raw):
-        """Iterator that undoes the effect of filtering, and yields
-        each row in serialised format (as a sequence of bytes).
-        Assumes input is straightlaced.  `raw` should be an iterable
-        that yields the raw bytes in chunks of arbitrary size.
+        shifts = [self.bitdepth * i
+                  for i in reversed(list(range(spb)))]
+        for o in bs:
+            out.extend([mask & (o >> i) for i in shifts])
+        return out[:width]
+
+    def _iter_straight_packed(self, byte_blocks):
+        """Iterator that undoes the effect of filtering;
+        yields each row as a sequence of packed bytes.
+        Assumes input is straightlaced.
+        `byte_blocks` should be an iterable that yields the raw bytes
+        in blocks of arbitrary size.
         """
 
         # length of row, in bytes
         rb = self.row_bytes
-        a = array('B')
-        # The previous (reconstructed) scanline.  None indicates first
-        # line of image.
+        a = bytearray()
+        # The previous (reconstructed) scanline.
+        # None indicates first line of image.
         recon = None
-        for some in raw:
-            a.extend(some)
+        for some_bytes in byte_blocks:
+            a.extend(some_bytes)
             while len(a) >= rb + 1:
                 filter_type = a[0]
-                scanline = a[1:rb+1]
-                del a[:rb+1]
+                scanline = a[1: rb + 1]
+                del a[: rb + 1]
                 recon = self.undo_filter(filter_type, scanline, recon)
                 yield recon
         if len(a) != 0:
             # :file:format We get here with a file format error:
             # when the available bytes (after decompressing) do not
             # pack into exact rows.
-            raise FormatError(
-              'Wrong size for decompressed IDAT chunk.')
+            raise FormatError('Wrong size for decompressed IDAT chunk.')
         assert len(a) == 0
 
     def validate_signature(self):
-        """If signature (header) has not been read then read and
+        """
+        If signature (header) has not been read then read and
         validate it; otherwise do nothing.
         """
 
         if self.signature:
             return
         self.signature = self.file.read(8)
-        if self.signature != _signature:
+        if self.signature != signature:
             raise FormatError("PNG file has invalid signature.")
 
     def preamble(self, lenient=False):
         """
-        Extract the image metadata by reading the initial part of
-        the PNG file up to the start of the ``IDAT`` chunk.  All the
-        chunks that precede the ``IDAT`` chunk are read and either
-        processed for metadata or discarded.
+        Extract the image metadata by reading
+        the initial part of the PNG file up to
+        the start of the ``IDAT`` chunk.
+        All the chunks that precede the ``IDAT`` chunk are
+        read and either processed for metadata or discarded.
 
-        If the optional `lenient` argument evaluates to `True`, checksum
-        failures will raise warnings rather than exceptions.
+        If the optional `lenient` argument evaluates to `True`,
+        checksum failures will raise warnings rather than exceptions.
         """
 
         self.validate_signature()
 
         while True:
             if not self.atchunk:
-                self.atchunk = self.chunklentype()
+                self.atchunk = self._chunk_len_type()
                 if self.atchunk is None:
-                    raise FormatError(
-                      'This PNG file has no IDAT chunks.')
+                    raise FormatError('This PNG file has no IDAT chunks.')
             if self.atchunk[1] == b'IDAT':
                 return
             self.process_chunk(lenient=lenient)
 
-    def chunklentype(self):
-        """Reads just enough of the input to determine the next
-        chunk's length and type, returned as a (*length*, *type*) pair
-        where *type* is a string.  If there are no more chunks, ``None``
-        is returned.
+    def _chunk_len_type(self):
+        """
+        Reads just enough of the input to
+        determine the next chunk's length and type;
+        return a (*length*, *type*) pair where *type* is a byte sequence.
+        If there are no more chunks, ``None`` is returned.
         """
 
         x = self.file.read(8)
@@ -1764,23 +1631,32 @@ class Reader:
             return None
         if len(x) != 8:
             raise FormatError(
-              'End of file whilst reading chunk length and type.')
-        length,type = struct.unpack('!I4s', x)
-        if length > 2**31-1:
-            raise FormatError('Chunk %s is too large: %d.' % (type,length))
-        return length,type
+                'End of file whilst reading chunk length and type.')
+        length, type = struct.unpack('!I4s', x)
+        if length > 2 ** 31 - 1:
+            raise FormatError('Chunk %s is too large: %d.' % (type, length))
+        # Check that all bytes are in valid ASCII range.
+        # https://www.w3.org/TR/2003/REC-PNG-20031110/#5Chunk-layout
+        type_bytes = set(bytearray(type))
+        if not(type_bytes <= set(range(65, 91)) | set(range(97, 123))):
+            raise FormatError(
+                'Chunk %r has invalid Chunk Type.'
+                % list(type))
+        return length, type
 
     def process_chunk(self, lenient=False):
-        """Process the next chunk and its data.  This only processes the
-        following chunk types, all others are ignored: ``IHDR``,
-        ``PLTE``, ``bKGD``, ``tRNS``, ``gAMA``, ``sBIT``, ``pHYs``.
+        """
+        Process the next chunk and its data.
+        This only processes the following chunk types:
+        ``IHDR``, ``PLTE``, ``bKGD``, ``tRNS``, ``gAMA``, ``sBIT``, ``pHYs``.
+        All other chunk types are ignored.
 
         If the optional `lenient` argument evaluates to `True`,
         checksum failures will raise warnings rather than exceptions.
         """
 
         type, data = self.chunk(lenient=lenient)
-        method = '_process_' + as_str(type)
+        method = '_process_' + type.decode('ascii')
         m = getattr(self, method, None)
         if m:
             m(data)
@@ -1796,22 +1672,26 @@ class Reader:
         check_bitdepth_colortype(self.bitdepth, self.color_type)
 
         if self.compression != 0:
-            raise Error("unknown compression method %d" % self.compression)
+            raise FormatError(
+                "Unknown compression method %d" % self.compression)
         if self.filter != 0:
-            raise FormatError("Unknown filter method %d,"
-              " see http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters ."
-              % self.filter)
-        if self.interlace not in (0,1):
-            raise FormatError("Unknown interlace method %d,"
-              " see http://www.w3.org/TR/2003/REC-PNG-20031110/#8InterlaceMethods ."
-              % self.interlace)
+            raise FormatError(
+                "Unknown filter method %d,"
+                " see http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters ."
+                % self.filter)
+        if self.interlace not in (0, 1):
+            raise FormatError(
+                "Unknown interlace method %d, see "
+                "http://www.w3.org/TR/2003/REC-PNG-20031110/#8InterlaceMethods"
+                " ."
+                % self.interlace)
 
         # Derived values
         # http://www.w3.org/TR/PNG/#6Colour-values
-        colormap =  bool(self.color_type & 1)
-        greyscale = not (self.color_type & 2)
+        colormap = bool(self.color_type & 1)
+        greyscale = not(self.color_type & 2)
         alpha = bool(self.color_type & 4)
-        color_planes = (3,1)[greyscale or colormap]
+        color_planes = (3, 1)[greyscale or colormap]
         planes = color_planes + alpha
 
         self.colormap = colormap
@@ -1819,7 +1699,7 @@ class Reader:
         self.alpha = alpha
         self.color_planes = color_planes
         self.planes = planes
-        self.psize = float(self.bitdepth)/float(8) * planes
+        self.psize = float(self.bitdepth) / float(8) * planes
         if int(self.psize) == self.psize:
             self.psize = int(self.psize)
         self.row_bytes = int(math.ceil(self.width * self.psize))
@@ -1829,7 +1709,7 @@ class Reader:
         # Stores tRNS chunk if present, and is used to check chunk
         # ordering constraints.
         self.trns = None
-        # Stores sbit chunk if present.
+        # Stores sBIT chunk if present.
         self.sbit = None
 
     def _process_PLTE(self, data):
@@ -1839,8 +1719,8 @@ class Reader:
         self.plte = data
         if len(data) % 3 != 0:
             raise FormatError(
-              "PLTE chunk's length should be a multiple of 3.")
-        if len(data) > (2**self.bitdepth)*3:
+                "PLTE chunk's length should be a multiple of 3.")
+        if len(data) > (2 ** self.bitdepth) * 3:
             raise FormatError("PLTE chunk is too long.")
         if len(data) == 0:
             raise FormatError("Empty PLTE is not allowed.")
@@ -1850,11 +1730,11 @@ class Reader:
             if self.colormap:
                 if not self.plte:
                     warnings.warn(
-                      "PLTE chunk is required before bKGD chunk.")
+                        "PLTE chunk is required before bKGD chunk.")
                 self.background = struct.unpack('B', data)
             else:
                 self.background = struct.unpack("!%dH" % self.color_planes,
-                  data)
+                                                data)
         except struct.error:
             raise FormatError("bKGD chunk has incorrect length.")
 
@@ -1865,15 +1745,15 @@ class Reader:
             if not self.plte:
                 warnings.warn("PLTE chunk is required before tRNS chunk.")
             else:
-                if len(data) > len(self.plte)/3:
+                if len(data) > len(self.plte) / 3:
                     # Was warning, but promoted to Error as it
                     # would otherwise cause pain later on.
                     raise FormatError("tRNS chunk is too long.")
         else:
             if self.alpha:
                 raise FormatError(
-                  "tRNS chunk is not valid with colour type %d." %
-                  self.color_type)
+                    "tRNS chunk is not valid with colour type %d." %
+                    self.color_type)
             try:
                 self.transparent = \
                     struct.unpack("!%dH" % self.color_planes, data)
@@ -1889,7 +1769,7 @@ class Reader:
     def _process_sBIT(self, data):
         self.sbit = data
         if (self.colormap and len(data) != 3 or
-            not self.colormap and len(data) != self.planes):
+                not self.colormap and len(data) != self.planes):
             raise FormatError("sBIT chunk has incorrect length.")
 
     def _process_pHYs(self, data):
@@ -1898,17 +1778,19 @@ class Reader:
         fmt = "!LLB"
         if len(data) != struct.calcsize(fmt):
             raise FormatError("pHYs chunk has incorrect length.")
-        self.x_pixels_per_unit, self.y_pixels_per_unit, unit = struct.unpack(fmt,data)
+        self.x_pixels_per_unit, self.y_pixels_per_unit, unit = \
+            struct.unpack(fmt, data)
         self.unit_is_meter = bool(unit)
 
     def read(self, lenient=False):
         """
-        Read the PNG file and decode it.  Returns (`width`, `height`,
-        `pixels`, `metadata`).
+        Read the PNG file and decode it.
+        Returns (`width`, `height`, `rows`, `info`).
 
         May use excessive memory.
 
-        `pixels` are returned in boxed row flat pixel format.
+        `rows` is a sequence of rows;
+        each row is a sequence of values.
 
         If the optional `lenient` argument evaluates to True,
         checksum failures will raise warnings rather than exceptions.
@@ -1917,10 +1799,7 @@ class Reader:
         def iteridat():
             """Iterator that yields all the ``IDAT`` chunks as strings."""
             while True:
-                try:
-                    type, data = self.chunk(lenient=lenient)
-                except ValueError as e:
-                    raise ChunkError(e.args[0])
+                type, data = self.chunk(lenient=lenient)
                 if type == b'IEND':
                     # http://www.w3.org/TR/PNG/#11IEND
                     break
@@ -1932,73 +1811,71 @@ class Reader:
                     warnings.warn("PLTE chunk is required before IDAT chunk")
                 yield data
 
-        def iterdecomp(idat):
-            """Iterator that yields decompressed strings.  `idat` should
-            be an iterator that yields the ``IDAT`` chunk data.
-            """
-
-            # Currently, with no max_length parameter to decompress,
-            # this routine will do one yield per IDAT chunk: Not very
-            # incremental.
-            d = zlib.decompressobj()
-            # Each IDAT chunk is passed to the decompressor, then any
-            # remaining state is decompressed out.
-            for data in idat:
-                # :todo: add a max_length argument here to limit output
-                # size.
-                yield array('B', d.decompress(data))
-            yield array('B', d.flush())
-
         self.preamble(lenient=lenient)
-        raw = iterdecomp(iteridat())
+        raw = decompress(iteridat())
 
         if self.interlace:
-            raw = array('B', itertools.chain(*raw))
-            arraycode = 'BH'[self.bitdepth>8]
-            # Like :meth:`group` but producing an array.array object for
-            # each row.
-            pixels = map(lambda *row: array(arraycode, row),
-                       *[iter(self.deinterlace(raw))]*self.width*self.planes)
+            def rows_from_interlace():
+                """Yield each row from an interlaced PNG."""
+                # It's important that this iterator doesn't read
+                # IDAT chunks until it yields the first row.
+                bs = bytearray(itertools.chain(*raw))
+                arraycode = 'BH'[self.bitdepth > 8]
+                # Like :meth:`group` but
+                # producing an array.array object for each row.
+                values = self._deinterlace(bs)
+                vpr = self.width * self.planes
+                for i in range(0, len(values), vpr):
+                    row = array(arraycode, values[i:i+vpr])
+                    yield row
+            rows = rows_from_interlace()
         else:
-            pixels = self.iterboxed(self.iterstraight(raw))
-        meta = dict()
+            rows = self._iter_bytes_to_values(self._iter_straight_packed(raw))
+        info = dict()
         for attr in 'greyscale alpha planes bitdepth interlace'.split():
-            meta[attr] = getattr(self, attr)
-        meta['size'] = (self.width, self.height)
+            info[attr] = getattr(self, attr)
+        info['size'] = (self.width, self.height)
         for attr in 'gamma transparent background'.split():
             a = getattr(self, attr, None)
             if a is not None:
-                meta[attr] = a
+                info[attr] = a
+        if getattr(self, 'x_pixels_per_unit', None):
+            info['physical'] = Resolution(self.x_pixels_per_unit,
+                                          self.y_pixels_per_unit,
+                                          self.unit_is_meter)
         if self.plte:
-            meta['palette'] = self.palette()
-        return self.width, self.height, pixels, meta
-
+            info['palette'] = self.palette()
+        return self.width, self.height, rows, info
 
     def read_flat(self):
         """
-        Read a PNG file and decode it into flat row flat pixel format.
-        Returns (*width*, *height*, *pixels*, *metadata*).
+        Read a PNG file and decode it into a single array of values.
+        Returns (*width*, *height*, *values*, *info*).
 
         May use excessive memory.
 
-        `pixels` are returned in flat row flat pixel format.
+        `values` is a single array.
 
-        See also the :meth:`read` method which returns pixels in the
-        more stream-friendly boxed row flat pixel format.
+        The :meth:`read` method is more stream-friendly than this,
+        because it returns a sequence of rows.
         """
 
-        x, y, pixel, meta = self.read()
-        arraycode = 'BH'[meta['bitdepth']>8]
+        x, y, pixel, info = self.read()
+        arraycode = 'BH'[info['bitdepth'] > 8]
         pixel = array(arraycode, itertools.chain(*pixel))
-        return x, y, pixel, meta
+        return x, y, pixel, info
 
     def palette(self, alpha='natural'):
-        """Returns a palette that is a sequence of 3-tuples or 4-tuples,
-        synthesizing it from the ``PLTE`` and ``tRNS`` chunks.  These
-        chunks should have already been processed (for example, by
-        calling the :meth:`preamble` method).  All the tuples are the
-        same size: 3-tuples if there is no ``tRNS`` chunk, 4-tuples when
-        there is a ``tRNS`` chunk.  Assumes that the image is colour type
+        """
+        Returns a palette that is a sequence of 3-tuples or 4-tuples,
+        synthesizing it from the ``PLTE`` and ``tRNS`` chunks.
+        These chunks should have already been processed (for example,
+        by calling the :meth:`preamble` method).
+        All the tuples are the same size:
+        3-tuples if there is no ``tRNS`` chunk,
+        4-tuples when there is a ``tRNS`` chunk.
+
+        Assumes that the image is colour type
         3 and therefore a ``PLTE`` chunk is required.
 
         If the `alpha` argument is ``'force'`` then an alpha channel is
@@ -2011,44 +1888,51 @@ class Reader:
         plte = group(array('B', self.plte), 3)
         if self.trns or alpha == 'force':
             trns = array('B', self.trns or [])
-            trns.extend([255]*(len(plte)-len(trns)))
+            trns.extend([255] * (len(plte) - len(trns)))
             plte = list(map(operator.add, plte, group(trns, 1)))
         return plte
 
     def asDirect(self):
-        """Returns the image data as a direct representation of an
-        ``x * y * planes`` array.  This method is intended to remove the
-        need for callers to deal with palettes and transparency
-        themselves.  Images with a palette (colour type 3)
-        are converted to RGB or RGBA; images with transparency (a
-        ``tRNS`` chunk) are converted to LA or RGBA as appropriate.
-        When returned in this format the pixel values represent the
-        colour value directly without needing to refer to palettes or
-        transparency information.
+        """
+        Returns the image data as a direct representation of
+        an ``x * y * planes`` array.
+        This removes the need for callers to deal with
+        palettes and transparency themselves.
+        Images with a palette (colour type 3) are converted to RGB or RGBA;
+        images with transparency (a ``tRNS`` chunk) are converted to
+        LA or RGBA as appropriate.
+        When returned in this format the pixel values represent
+        the colour value directly without needing to refer
+        to palettes or transparency information.
 
         Like the :meth:`read` method this method returns a 4-tuple:
 
-        (*width*, *height*, *pixels*, *meta*)
-
-        This method normally returns pixel values with the bit depth
-        they have in the source image, but when the source PNG has an
-        ``sBIT`` chunk it is inspected and can reduce the bit depth of
-        the result pixels; pixel values will be reduced according to
-        the bit depth specified in the ``sBIT`` chunk (PNG nerds should
-        note a single result bit depth is used for all channels; the
-        maximum of the ones specified in the ``sBIT`` chunk.  An RGB565
-        image will be rescaled to 6-bit RGB666).
-
-        The *meta* dictionary that is returned reflects the `direct`
-        format and not the original source image.  For example, an RGB
-        source image with a ``tRNS`` chunk to represent a transparent
-        colour, will have ``planes=3`` and ``alpha=False`` for the
-        source image, but the *meta* dictionary returned by this method
-        will have ``planes=4`` and ``alpha=True`` because an alpha
-        channel is synthesized and added.
-
-        *pixels* is the pixel data in boxed row flat pixel format (just
-        like the :meth:`read` method).
+        (*width*, *height*, *rows*, *info*)
+
+        This method normally returns pixel values with
+        the bit depth they have in the source image, but
+        when the source PNG has an ``sBIT`` chunk it is inspected and
+        can reduce the bit depth of the result pixels;
+        pixel values will be reduced according to the bit depth
+        specified in the ``sBIT`` chunk.
+        PNG nerds should note a single result bit depth is
+        used for all channels:
+        the maximum of the ones specified in the ``sBIT`` chunk.
+        An RGB565 image will be rescaled to 6-bit RGB666.
+
+        The *info* dictionary that is returned reflects
+        the `direct` format and not the original source image.
+        For example, an RGB source image with a ``tRNS`` chunk
+        to represent a transparent colour,
+        will start with ``planes=3`` and ``alpha=False`` for the
+        source image,
+        but the *info* dictionary returned by this method
+        will have ``planes=4`` and ``alpha=True`` because
+        an alpha channel is synthesized and added.
+
+        *rows* is a sequence of rows;
+        each row being a sequence of values
+        (like the :meth:`read` method).
 
         All the other aspects of the image data are not changed.
         """
@@ -2059,14 +1943,15 @@ class Reader:
         if not self.colormap and not self.trns and not self.sbit:
             return self.read()
 
-        x,y,pixels,meta = self.read()
+        x, y, pixels, info = self.read()
 
         if self.colormap:
-            meta['colormap'] = False
-            meta['alpha'] = bool(self.trns)
-            meta['bitdepth'] = 8
-            meta['planes'] = 3 + bool(self.trns)
+            info['colormap'] = False
+            info['alpha'] = bool(self.trns)
+            info['bitdepth'] = 8
+            info['planes'] = 3 + bool(self.trns)
             plte = self.palette()
+
             def iterpal(pixels):
                 for row in pixels:
                     row = [plte[x] for x in row]
@@ -2081,11 +1966,12 @@ class Reader:
             # perhaps go faster (all those 1-tuples!), but I still
             # wonder whether the code proliferation is worth it)
             it = self.transparent
-            maxval = 2**meta['bitdepth']-1
-            planes = meta['planes']
-            meta['alpha'] = True
-            meta['planes'] += 1
-            typecode = 'BH'[meta['bitdepth']>8]
+            maxval = 2 ** info['bitdepth'] - 1
+            planes = info['planes']
+            info['alpha'] = True
+            info['planes'] += 1
+            typecode = 'BH'[info['bitdepth'] > 8]
+
             def itertrns(pixels):
                 for row in pixels:
                     # For each row we group it into pixels, then form a
@@ -2096,142 +1982,147 @@ class Reader:
                     row = group(row, planes)
                     opa = map(it.__ne__, row)
                     opa = map(maxval.__mul__, opa)
-                    opa = list(zip(opa)) # convert to 1-tuples
-                    yield array(typecode,
-                      itertools.chain(*map(operator.add, row, opa)))
+                    opa = list(zip(opa))    # convert to 1-tuples
+                    yield array(
+                        typecode,
+                        itertools.chain(*map(operator.add, row, opa)))
             pixels = itertrns(pixels)
         targetbitdepth = None
         if self.sbit:
             sbit = struct.unpack('%dB' % len(self.sbit), self.sbit)
             targetbitdepth = max(sbit)
-            if targetbitdepth > meta['bitdepth']:
+            if targetbitdepth > info['bitdepth']:
                 raise Error('sBIT chunk %r exceeds bitdepth %d' %
-                    (sbit,self.bitdepth))
+                            (sbit, self.bitdepth))
             if min(sbit) <= 0:
                 raise Error('sBIT chunk %r has a 0-entry' % sbit)
-            if targetbitdepth == meta['bitdepth']:
-                targetbitdepth = None
         if targetbitdepth:
-            shift = meta['bitdepth'] - targetbitdepth
-            meta['bitdepth'] = targetbitdepth
+            shift = info['bitdepth'] - targetbitdepth
+            info['bitdepth'] = targetbitdepth
+
             def itershift(pixels):
                 for row in pixels:
                     yield [p >> shift for p in row]
             pixels = itershift(pixels)
-        return x,y,pixels,meta
-
-    def asFloat(self, maxval=1.0):
-        """Return image pixels as per :meth:`asDirect` method, but scale
-        all pixel values to be floating point values between 0.0 and
-        *maxval*.
-        """
-
-        x,y,pixels,info = self.asDirect()
-        sourcemaxval = 2**info['bitdepth']-1
-        del info['bitdepth']
-        info['maxval'] = float(maxval)
-        factor = float(maxval)/float(sourcemaxval)
-        def iterfloat():
-            for row in pixels:
-                yield [factor * p for p in row]
-        return x,y,iterfloat(),info
+        return x, y, pixels, info
 
     def _as_rescale(self, get, targetbitdepth):
         """Helper used by :meth:`asRGB8` and :meth:`asRGBA8`."""
 
-        width,height,pixels,meta = get()
-        maxval = 2**meta['bitdepth'] - 1
+        width, height, pixels, info = get()
+        maxval = 2**info['bitdepth'] - 1
         targetmaxval = 2**targetbitdepth - 1
         factor = float(targetmaxval) / float(maxval)
-        meta['bitdepth'] = targetbitdepth
+        info['bitdepth'] = targetbitdepth
+
         def iterscale():
             for row in pixels:
-                yield [int(round(x*factor)) for x in row]
+                yield [int(round(x * factor)) for x in row]
         if maxval == targetmaxval:
-            return width, height, pixels, meta
+            return width, height, pixels, info
         else:
-            return width, height, iterscale(), meta
+            return width, height, iterscale(), info
 
     def asRGB8(self):
-        """Return the image data as an RGB pixels with 8-bits per
-        sample.  This is like the :meth:`asRGB` method except that
-        this method additionally rescales the values so that they
-        are all between 0 and 255 (8-bit).  In the case where the
-        source image has a bit depth < 8 the transformation preserves
-        all the information; where the source image has bit depth
-        > 8, then rescaling to 8-bit values loses precision.  No
-        dithering is performed.  Like :meth:`asRGB`, an alpha channel
-        in the source image will raise an exception.
+        """
+        Return the image data as an RGB pixels with 8-bits per sample.
+        This is like the :meth:`asRGB` method except that
+        this method additionally rescales the values so that
+        they are all between 0 and 255 (8-bit).
+        In the case where the source image has a bit depth < 8
+        the transformation preserves all the information;
+        where the source image has bit depth > 8, then
+        rescaling to 8-bit values loses precision.
+        No dithering is performed.
+        Like :meth:`asRGB`,
+        an alpha channel in the source image will raise an exception.
 
         This function returns a 4-tuple:
-        (*width*, *height*, *pixels*, *metadata*).
-        *width*, *height*, *metadata* are as per the
-        :meth:`read` method.
-        
-        *pixels* is the pixel data in boxed row flat pixel format.
+        (*width*, *height*, *rows*, *info*).
+        *width*, *height*, *info* are as per the :meth:`read` method.
+
+        *rows* is the pixel data as a sequence of rows.
         """
 
         return self._as_rescale(self.asRGB, 8)
 
     def asRGBA8(self):
-        """Return the image data as RGBA pixels with 8-bits per
-        sample.  This method is similar to :meth:`asRGB8` and
-        :meth:`asRGBA`:  The result pixels have an alpha channel, *and*
-        values are rescaled to the range 0 to 255.  The alpha channel is
-        synthesized if necessary (with a small speed penalty).
+        """
+        Return the image data as RGBA pixels with 8-bits per sample.
+        This method is similar to :meth:`asRGB8` and :meth:`asRGBA`:
+        The result pixels have an alpha channel, *and*
+        values are rescaled to the range 0 to 255.
+        The alpha channel is synthesized if necessary
+        (with a small speed penalty).
         """
 
         return self._as_rescale(self.asRGBA, 8)
 
     def asRGB(self):
-        """Return image as RGB pixels.  RGB colour images are passed
-        through unchanged; greyscales are expanded into RGB
-        triplets (there is a small speed overhead for doing this).
+        """
+        Return image as RGB pixels.
+        RGB colour images are passed through unchanged;
+        greyscales are expanded into RGB triplets
+        (there is a small speed overhead for doing this).
 
-        An alpha channel in the source image will raise an
-        exception.
+        An alpha channel in the source image will raise an exception.
 
-        The return values are as for the :meth:`read` method
-        except that the *metadata* reflect the returned pixels, not the
-        source image.  In particular, for this method
-        ``metadata['greyscale']`` will be ``False``.
+        The return values are as for the :meth:`read` method except that
+        the *info* reflect the returned pixels, not the source image.
+        In particular,
+        for this method ``info['greyscale']`` will be ``False``.
         """
 
-        width,height,pixels,meta = self.asDirect()
-        if meta['alpha']:
+        width, height, pixels, info = self.asDirect()
+        if info['alpha']:
             raise Error("will not convert image with alpha channel to RGB")
-        if not meta['greyscale']:
-            return width,height,pixels,meta
-        meta['greyscale'] = False
-        typecode = 'BH'[meta['bitdepth'] > 8]
+        if not info['greyscale']:
+            return width, height, pixels, info
+        info['greyscale'] = False
+        info['planes'] = 3
+
+        if info['bitdepth'] > 8:
+            def newarray():
+                return array('H', [0])
+        else:
+            def newarray():
+                return bytearray([0])
+
         def iterrgb():
             for row in pixels:
-                a = array(typecode, [0]) * 3 * width
+                a = newarray() * 3 * width
                 for i in range(3):
                     a[i::3] = row
                 yield a
-        return width,height,iterrgb(),meta
+        return width, height, iterrgb(), info
 
     def asRGBA(self):
-        """Return image as RGBA pixels.  Greyscales are expanded into
-        RGB triplets; an alpha channel is synthesized if necessary.
-        The return values are as for the :meth:`read` method
-        except that the *metadata* reflect the returned pixels, not the
-        source image.  In particular, for this method
-        ``metadata['greyscale']`` will be ``False``, and
-        ``metadata['alpha']`` will be ``True``.
+        """
+        Return image as RGBA pixels.
+        Greyscales are expanded into RGB triplets;
+        an alpha channel is synthesized if necessary.
+        The return values are as for the :meth:`read` method except that
+        the *info* reflect the returned pixels, not the source image.
+        In particular, for this method
+        ``info['greyscale']`` will be ``False``, and
+        ``info['alpha']`` will be ``True``.
         """
 
-        width,height,pixels,meta = self.asDirect()
-        if meta['alpha'] and not meta['greyscale']:
-            return width,height,pixels,meta
-        typecode = 'BH'[meta['bitdepth'] > 8]
-        maxval = 2**meta['bitdepth'] - 1
+        width, height, pixels, info = self.asDirect()
+        if info['alpha'] and not info['greyscale']:
+            return width, height, pixels, info
+        typecode = 'BH'[info['bitdepth'] > 8]
+        maxval = 2**info['bitdepth'] - 1
         maxbuffer = struct.pack('=' + typecode, maxval) * 4 * width
-        def newarray():
-            return array(typecode, maxbuffer)
 
-        if meta['alpha'] and meta['greyscale']:
+        if info['bitdepth'] > 8:
+            def newarray():
+                return array('H', maxbuffer)
+        else:
+            def newarray():
+                return bytearray(maxbuffer)
+
+        if info['alpha'] and info['greyscale']:
             # LA to RGBA
             def convert():
                 for row in pixels:
@@ -2239,452 +2130,218 @@ class Reader:
                     # into first three target channels, and A channel
                     # into fourth channel.
                     a = newarray()
-                    pngfilters.convert_la_to_rgba(row, a)
+                    convert_la_to_rgba(row, a)
                     yield a
-        elif meta['greyscale']:
+        elif info['greyscale']:
             # L to RGBA
             def convert():
                 for row in pixels:
                     a = newarray()
-                    pngfilters.convert_l_to_rgba(row, a)
+                    convert_l_to_rgba(row, a)
                     yield a
         else:
-            assert not meta['alpha'] and not meta['greyscale']
+            assert not info['alpha'] and not info['greyscale']
             # RGB to RGBA
+
             def convert():
                 for row in pixels:
                     a = newarray()
-                    pngfilters.convert_rgb_to_rgba(row, a)
+                    convert_rgb_to_rgba(row, a)
                     yield a
-        meta['alpha'] = True
-        meta['greyscale'] = False
-        return width,height,convert(),meta
+        info['alpha'] = True
+        info['greyscale'] = False
+        info['planes'] = 4
+        return width, height, convert(), info
+
+
+def decompress(data_blocks):
+    """
+    `data_blocks` should be an iterable that
+    yields the compressed data (from the ``IDAT`` chunks).
+    This yields decompressed byte strings.
+    """
+
+    # Currently, with no max_length parameter to decompress,
+    # this routine will do one yield per IDAT chunk: Not very
+    # incremental.
+    d = zlib.decompressobj()
+    # Each IDAT chunk is passed to the decompressor, then any
+    # remaining state is decompressed out.
+    for data in data_blocks:
+        # :todo: add a max_length argument here to limit output size.
+        yield bytearray(d.decompress(data))
+    yield bytearray(d.flush())
+
 
 def check_bitdepth_colortype(bitdepth, colortype):
-    """Check that `bitdepth` and `colortype` are both valid,
-    and specified in a valid combination. Returns if valid,
-    raise an Exception if not valid.
+    """
+    Check that `bitdepth` and `colortype` are both valid,
+    and specified in a valid combination.
+    Returns (None) if valid, raise an Exception if not valid.
     """
 
-    if bitdepth not in (1,2,4,8,16):
+    if bitdepth not in (1, 2, 4, 8, 16):
         raise FormatError("invalid bit depth %d" % bitdepth)
-    if colortype not in (0,2,3,4,6):
+    if colortype not in (0, 2, 3, 4, 6):
         raise FormatError("invalid colour type %d" % colortype)
     # Check indexed (palettized) images have 8 or fewer bits
     # per pixel; check only indexed or greyscale images have
     # fewer than 8 bits per pixel.
     if colortype & 1 and bitdepth > 8:
         raise FormatError(
-          "Indexed images (colour type %d) cannot"
-          " have bitdepth > 8 (bit depth %d)."
-          " See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ."
-          % (bitdepth, colortype))
-    if bitdepth < 8 and colortype not in (0,3):
-        raise FormatError("Illegal combination of bit depth (%d)"
-          " and colour type (%d)."
-          " See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ."
-          % (bitdepth, colortype))
-
-def isinteger(x):
+            "Indexed images (colour type %d) cannot"
+            " have bitdepth > 8 (bit depth %d)."
+            " See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ."
+            % (bitdepth, colortype))
+    if bitdepth < 8 and colortype not in (0, 3):
+        raise FormatError(
+            "Illegal combination of bit depth (%d)"
+            " and colour type (%d)."
+            " See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ."
+            % (bitdepth, colortype))
+
+
+def is_natural(x):
+    """A non-negative integer."""
     try:
-        return int(x) == x
+        is_integer = int(x) == x
     except (TypeError, ValueError):
         return False
+    return is_integer and x >= 0
 
 
-# === Support for users without Cython ===
-
-try:
-    pngfilters
-except NameError:
-    class pngfilters:
-        def undo_filter_sub(filter_unit, scanline, previous, result):
-            """Undo sub filter."""
-
-            ai = 0
-            # Loops starts at index fu.  Observe that the initial part
-            # of the result is already filled in correctly with
-            # scanline.
-            for i in range(filter_unit, len(result)):
-                x = scanline[i]
-                a = result[ai]
-                result[i] = (x + a) & 0xff
-                ai += 1
-        undo_filter_sub = staticmethod(undo_filter_sub)
-
-        def undo_filter_up(filter_unit, scanline, previous, result):
-            """Undo up filter."""
-
-            for i in range(len(result)):
-                x = scanline[i]
-                b = previous[i]
-                result[i] = (x + b) & 0xff
-        undo_filter_up = staticmethod(undo_filter_up)
-
-        def undo_filter_average(filter_unit, scanline, previous, result):
-            """Undo up filter."""
-
-            ai = -filter_unit
-            for i in range(len(result)):
-                x = scanline[i]
-                if ai < 0:
-                    a = 0
-                else:
-                    a = result[ai]
-                b = previous[i]
-                result[i] = (x + ((a + b) >> 1)) & 0xff
-                ai += 1
-        undo_filter_average = staticmethod(undo_filter_average)
-
-        def undo_filter_paeth(filter_unit, scanline, previous, result):
-            """Undo Paeth filter."""
-
-            # Also used for ci.
-            ai = -filter_unit
-            for i in range(len(result)):
-                x = scanline[i]
-                if ai < 0:
-                    a = c = 0
-                else:
-                    a = result[ai]
-                    c = previous[ai]
-                b = previous[i]
-                p = a + b - c
-                pa = abs(p - a)
-                pb = abs(p - b)
-                pc = abs(p - c)
-                if pa <= pb and pa <= pc:
-                    pr = a
-                elif pb <= pc:
-                    pr = b
-                else:
-                    pr = c
-                result[i] = (x + pr) & 0xff
-                ai += 1
-        undo_filter_paeth = staticmethod(undo_filter_paeth)
-
-        def convert_la_to_rgba(row, result):
-            for i in range(3):
-                result[i::4] = row[0::2]
-            result[3::4] = row[1::2]
-        convert_la_to_rgba = staticmethod(convert_la_to_rgba)
-
-        def convert_l_to_rgba(row, result):
-            """Convert a grayscale image to RGBA. This method assumes
-            the alpha channel in result is already correctly
-            initialized.
-            """
-            for i in range(3):
-                result[i::4] = row
-        convert_l_to_rgba = staticmethod(convert_l_to_rgba)
+def undo_filter_sub(filter_unit, scanline, previous, result):
+    """Undo sub filter."""
 
-        def convert_rgb_to_rgba(row, result):
-            """Convert an RGB image to RGBA. This method assumes the
-            alpha channel in result is already correctly initialized.
-            """
-            for i in range(3):
-                result[i::4] = row[i::3]
-        convert_rgb_to_rgba = staticmethod(convert_rgb_to_rgba)
+    ai = 0
+    # Loops starts at index fu.  Observe that the initial part
+    # of the result is already filled in correctly with
+    # scanline.
+    for i in range(filter_unit, len(result)):
+        x = scanline[i]
+        a = result[ai]
+        result[i] = (x + a) & 0xff
+        ai += 1
 
 
-# === Command Line Support ===
+def undo_filter_up(filter_unit, scanline, previous, result):
+    """Undo up filter."""
 
-def read_pam_header(infile):
-    """
-    Read (the rest of a) PAM header.  `infile` should be positioned
-    immediately after the initial 'P7' line (at the beginning of the
-    second line).  Returns are as for `read_pnm_header`.
-    """
-    
-    # Unlike PBM, PGM, and PPM, we can read the header a line at a time.
-    header = dict()
-    while True:
-        l = infile.readline().strip()
-        if l == b'ENDHDR':
-            break
-        if not l:
-            raise EOFError('PAM ended prematurely')
-        if l[0] == b'#':
-            continue
-        l = l.split(None, 1)
-        if l[0] not in header:
-            header[l[0]] = l[1]
+    for i in range(len(result)):
+        x = scanline[i]
+        b = previous[i]
+        result[i] = (x + b) & 0xff
+
+
+def undo_filter_average(filter_unit, scanline, previous, result):
+    """Undo up filter."""
+
+    ai = -filter_unit
+    for i in range(len(result)):
+        x = scanline[i]
+        if ai < 0:
+            a = 0
         else:
-            header[l[0]] += b' ' + l[1]
-
-    required = [b'WIDTH', b'HEIGHT', b'DEPTH', b'MAXVAL']
-    WIDTH,HEIGHT,DEPTH,MAXVAL = required
-    present = [x for x in required if x in header]
-    if len(present) != len(required):
-        raise Error('PAM file must specify WIDTH, HEIGHT, DEPTH, and MAXVAL')
-    width = int(header[WIDTH])
-    height = int(header[HEIGHT])
-    depth = int(header[DEPTH])
-    maxval = int(header[MAXVAL])
-    if (width <= 0 or
-        height <= 0 or
-        depth <= 0 or
-        maxval <= 0):
-        raise Error(
-          'WIDTH, HEIGHT, DEPTH, MAXVAL must all be positive integers')
-    return 'P7', width, height, depth, maxval
-
-def read_pnm_header(infile, supported=(b'P5', b'P6')):
-    """
-    Read a PNM header, returning (format,width,height,depth,maxval).
-    `width` and `height` are in pixels.  `depth` is the number of
-    channels in the image; for PBM and PGM it is synthesized as 1, for
-    PPM as 3; for PAM images it is read from the header.  `maxval` is
-    synthesized (as 1) for PBM images.
-    """
+            a = result[ai]
+        b = previous[i]
+        result[i] = (x + ((a + b) >> 1)) & 0xff
+        ai += 1
 
-    # Generally, see http://netpbm.sourceforge.net/doc/ppm.html
-    # and http://netpbm.sourceforge.net/doc/pam.html
-
-    # Technically 'P7' must be followed by a newline, so by using
-    # rstrip() we are being liberal in what we accept.  I think this
-    # is acceptable.
-    type = infile.read(3).rstrip()
-    if type not in supported:
-        raise NotImplementedError('file format %s not supported' % type)
-    if type == b'P7':
-        # PAM header parsing is completely different.
-        return read_pam_header(infile)
-    # Expected number of tokens in header (3 for P4, 4 for P6)
-    expected = 4
-    pbm = (b'P1', b'P4')
-    if type in pbm:
-        expected = 3
-    header = [type]
-
-    # We have to read the rest of the header byte by byte because the
-    # final whitespace character (immediately following the MAXVAL in
-    # the case of P6) may not be a newline.  Of course all PNM files in
-    # the wild use a newline at this point, so it's tempting to use
-    # readline; but it would be wrong.
-    def getc():
-        c = infile.read(1)
-        if not c:
-            raise Error('premature EOF reading PNM header')
-        return c
 
-    c = getc()
-    while True:
-        # Skip whitespace that precedes a token.
-        while c.isspace():
-            c = getc()
-        # Skip comments.
-        while c == '#':
-            while c not in b'\n\r':
-                c = getc()
-        if not c.isdigit():
-            raise Error('unexpected character %s found in header' % c)
-        # According to the specification it is legal to have comments
-        # that appear in the middle of a token.
-        # This is bonkers; I've never seen it; and it's a bit awkward to
-        # code good lexers in Python (no goto).  So we break on such
-        # cases.
-        token = b''
-        while c.isdigit():
-            token += c
-            c = getc()
-        # Slight hack.  All "tokens" are decimal integers, so convert
-        # them here.
-        header.append(int(token))
-        if len(header) == expected:
-            break
-    # Skip comments (again)
-    while c == '#':
-        while c not in '\n\r':
-            c = getc()
-    if not c.isspace():
-        raise Error('expected header to end with whitespace, not %s' % c)
-
-    if type in pbm:
-        # synthesize a MAXVAL
-        header.append(1)
-    depth = (1,3)[type == b'P6']
-    return header[0], header[1], header[2], depth, header[3]
-
-def write_pnm(file, width, height, pixels, meta):
-    """Write a Netpbm PNM/PAM file.
-    """
+def undo_filter_paeth(filter_unit, scanline, previous, result):
+    """Undo Paeth filter."""
 
-    bitdepth = meta['bitdepth']
-    maxval = 2**bitdepth - 1
-    # Rudely, the number of image planes can be used to determine
-    # whether we are L (PGM), LA (PAM), RGB (PPM), or RGBA (PAM).
-    planes = meta['planes']
-    # Can be an assert as long as we assume that pixels and meta came
-    # from a PNG file.
-    assert planes in (1,2,3,4)
-    if planes in (1,3):
-        if 1 == planes:
-            # PGM
-            # Could generate PBM if maxval is 1, but we don't (for one
-            # thing, we'd have to convert the data, not just blat it
-            # out).
-            fmt = 'P5'
+    # Also used for ci.
+    ai = -filter_unit
+    for i in range(len(result)):
+        x = scanline[i]
+        if ai < 0:
+            a = c = 0
         else:
-            # PPM
-            fmt = 'P6'
-        header = '%s %d %d %d\n' % (fmt, width, height, maxval)
-    if planes in (2,4):
-        # PAM
-        # See http://netpbm.sourceforge.net/doc/pam.html
-        if 2 == planes:
-            tupltype = 'GRAYSCALE_ALPHA'
+            a = result[ai]
+            c = previous[ai]
+        b = previous[i]
+        p = a + b - c
+        pa = abs(p - a)
+        pb = abs(p - b)
+        pc = abs(p - c)
+        if pa <= pb and pa <= pc:
+            pr = a
+        elif pb <= pc:
+            pr = b
         else:
-            tupltype = 'RGB_ALPHA'
-        header = ('P7\nWIDTH %d\nHEIGHT %d\nDEPTH %d\nMAXVAL %d\n'
-                  'TUPLTYPE %s\nENDHDR\n' %
-                  (width, height, planes, maxval, tupltype))
-    file.write(header.encode('ascii'))
-    # Values per row
-    vpr = planes * width
-    # struct format
-    fmt = '>%d' % vpr
-    if maxval > 0xff:
-        fmt = fmt + 'H'
-    else:
-        fmt = fmt + 'B'
-    for row in pixels:
-        file.write(struct.pack(fmt, *row))
-    file.flush()
+            pr = c
+        result[i] = (x + pr) & 0xff
+        ai += 1
+
+
+def convert_la_to_rgba(row, result):
+    for i in range(3):
+        result[i::4] = row[0::2]
+    result[3::4] = row[1::2]
+
 
-def color_triple(color):
+def convert_l_to_rgba(row, result):
     """
-    Convert a command line colour value to a RGB triple of integers.
-    FIXME: Somewhere we need support for greyscale backgrounds etc.
+    Convert a grayscale image to RGBA.
+    This method assumes the alpha channel in result is
+    already correctly initialized.
     """
-    if color.startswith('#') and len(color) == 4:
-        return (int(color[1], 16),
-                int(color[2], 16),
-                int(color[3], 16))
-    if color.startswith('#') and len(color) == 7:
-        return (int(color[1:3], 16),
-                int(color[3:5], 16),
-                int(color[5:7], 16))
-    elif color.startswith('#') and len(color) == 13:
-        return (int(color[1:5], 16),
-                int(color[5:9], 16),
-                int(color[9:13], 16))
-
-def _add_common_options(parser):
-    """Call *parser.add_option* for each of the options that are
-    common between this PNG--PNM conversion tool and the gen
-    tool.
+    for i in range(3):
+        result[i::4] = row
+
+
+def convert_rgb_to_rgba(row, result):
+    """
+    Convert an RGB image to RGBA.
+    This method assumes the alpha channel in result is
+    already correctly initialized.
     """
-    parser.add_option("-i", "--interlace",
-                      default=False, action="store_true",
-                      help="create an interlaced PNG file (Adam7)")
-    parser.add_option("-t", "--transparent",
-                      action="store", type="string", metavar="#RRGGBB",
-                      help="mark the specified colour as transparent")
-    parser.add_option("-b", "--background",
-                      action="store", type="string", metavar="#RRGGBB",
-                      help="save the specified background colour")
-    parser.add_option("-g", "--gamma",
-                      action="store", type="float", metavar="value",
-                      help="save the specified gamma value")
-    parser.add_option("-c", "--compression",
-                      action="store", type="int", metavar="level",
-                      help="zlib compression level (0-9)")
-    return parser
-
-def _main(argv):
+    for i in range(3):
+        result[i::4] = row[i::3]
+
+
+# Only reason to include this in this module is that
+# several utilities need it, and it is small.
+def binary_stdin():
     """
-    Run the PNG encoder with options from the command line.
+    A sys.stdin that returns bytes.
     """
 
-    # Parse command line arguments
-    from optparse import OptionParser
-    version = '%prog ' + __version__
-    parser = OptionParser(version=version)
-    parser.set_usage("%prog [options] [imagefile]")
-    parser.add_option('-r', '--read-png', default=False,
-                      action='store_true',
-                      help='Read PNG, write PNM')
-    parser.add_option("-a", "--alpha",
-                      action="store", type="string", metavar="pgmfile",
-                      help="alpha channel transparency (RGBA)")
-    _add_common_options(parser)
-
-    (options, args) = parser.parse_args(args=argv[1:])
-
-    # Convert options
-    if options.transparent is not None:
-        options.transparent = color_triple(options.transparent)
-    if options.background is not None:
-        options.background = color_triple(options.background)
-
-    # Prepare input and output files
-    if len(args) == 0:
-        infilename = '-'
-        infile = sys.stdin
-    elif len(args) == 1:
-        infilename = args[0]
-        infile = open(infilename, 'rb')
-    else:
-        parser.error("more than one input file")
-    outfile = sys.stdout
+    return sys.stdin.buffer
+
+
+def binary_stdout():
+    """
+    A sys.stdout that accepts bytes.
+    """
+
+    stdout = sys.stdout.buffer
+
+    # On Windows the C runtime file orientation needs changing.
     if sys.platform == "win32":
-        import msvcrt, os
+        import msvcrt
+        import os
         msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
 
-    if options.read_png:
-        # Encode PNG to PPM
-        png = Reader(file=infile)
-        width,height,pixels,meta = png.asDirect()
-        write_pnm(outfile, width, height, pixels, meta) 
-    else:
-        # Encode PNM to PNG
-        format, width, height, depth, maxval = \
-          read_pnm_header(infile, (b'P5',b'P6',b'P7'))
-        # When it comes to the variety of input formats, we do something
-        # rather rude.  Observe that L, LA, RGB, RGBA are the 4 colour
-        # types supported by PNG and that they correspond to 1, 2, 3, 4
-        # channels respectively.  So we use the number of channels in
-        # the source image to determine which one we have.  We do not
-        # care about TUPLTYPE.
-        greyscale = depth <= 2
-        pamalpha = depth in (2,4)
-        supported = [2**x-1 for x in range(1,17)]
-        try:
-            mi = supported.index(maxval)
-        except ValueError:
-            raise NotImplementedError(
-              'your maxval (%s) not in supported list %s' %
-              (maxval, str(supported)))
-        bitdepth = mi+1
-        writer = Writer(width, height,
-                        greyscale=greyscale,
-                        bitdepth=bitdepth,
-                        interlace=options.interlace,
-                        transparent=options.transparent,
-                        background=options.background,
-                        alpha=bool(pamalpha or options.alpha),
-                        gamma=options.gamma,
-                        compression=options.compression)
-        if options.alpha:
-            pgmfile = open(options.alpha, 'rb')
-            format, awidth, aheight, adepth, amaxval = \
-              read_pnm_header(pgmfile, 'P5')
-            if amaxval != '255':
-                raise NotImplementedError(
-                  'maxval %s not supported for alpha channel' % amaxval)
-            if (awidth, aheight) != (width, height):
-                raise ValueError("alpha channel image size mismatch"
-                                 " (%s has %sx%s but %s has %sx%s)"
-                                 % (infilename, width, height,
-                                    options.alpha, awidth, aheight))
-            writer.convert_ppm_and_pgm(infile, pgmfile, outfile)
-        else:
-            writer.convert_pnm(infile, outfile)
+    return stdout
+
+
+def cli_open(path):
+    if path == "-":
+        return binary_stdin()
+    return open(path, "rb")
+
+
+def main(argv):
+    """
+    Run command line PNG.
+    """
+    print("What should the command line tool do?", file=sys.stderr)
 
 
 if __name__ == '__main__':
     try:
-        _main(sys.argv)
+        main(sys.argv)
     except Error as e:
         print(e, file=sys.stderr)
diff --git a/pyglet/font/__init__.py b/pyglet/font/__init__.py
index 98308de..1831315 100644
--- a/pyglet/font/__init__.py
+++ b/pyglet/font/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -64,8 +64,17 @@ if not getattr(sys, 'is_pyglet_doc_run', False):
         _font_class = QuartzFont
 
     elif pyglet.compat_platform in ('win32', 'cygwin'):
-        from pyglet.font.win32 import GDIPlusFont
-        _font_class = GDIPlusFont
+        from pyglet.libs.win32.constants import WINDOWS_7_OR_GREATER
+        if WINDOWS_7_OR_GREATER:
+            if pyglet.options["advanced_font_features"] is True:
+                from pyglet.font.directwrite import Win32DirectWriteFont
+                _font_class = Win32DirectWriteFont
+            else:
+                from pyglet.font.win32 import GDIPlusFont
+                _font_class = GDIPlusFont
+        else:
+            from pyglet.font.win32 import GDIPlusFont
+            _font_class = GDIPlusFont
 
     else:
         from pyglet.font.freetype import FreeTypeFont
@@ -77,7 +86,7 @@ def have_font(name):
     return _font_class.have_font(name)
 
 
-def load(name=None, size=None, bold=False, italic=False, dpi=None):
+def load(name=None, size=None, bold=False, italic=False, stretch=False, dpi=None):
     """Load a font for rendering.
 
     :Parameters:
@@ -127,18 +136,19 @@ def load(name=None, size=None, bold=False, italic=False, dpi=None):
     font_hold = shared_object_space.pyglet_font_font_hold
 
     # Look for font name in font cache
-    descriptor = (name, size, bold, italic, dpi)
+    descriptor = (name, size, bold, italic, stretch, dpi)
     if descriptor in font_cache:
         return font_cache[descriptor]
 
     # Not in cache, create from scratch
-    font = _font_class(name, size, bold=bold, italic=italic, dpi=dpi)
+    font = _font_class(name, size, bold=bold, italic=italic, stretch=stretch, dpi=dpi)
 
     # Save parameters for new-style layout classes to recover
-    font.name = name
+    # TODO: add properties to the Font classes, so these can be queried:
     font.size = size
     font.bold = bold
     font.italic = italic
+    font.stretch = stretch
     font.dpi = dpi
 
     # Cache font in weak-ref dictionary to avoid reloading while still in use
diff --git a/pyglet/font/base.py b/pyglet/font/base.py
index 5f363b3..d3b7254 100644
--- a/pyglet/font/base.py
+++ b/pyglet/font/base.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -148,13 +148,18 @@ class Glyph(image.TextureRegion):
         `vertices` : (int, int, int, int)
             The vertices of this glyph, with (0,0) originating at the
             left-side bearing at the baseline.
+        `colored` : bool
+            If a glyph is colored by the font renderer, such as an emoji, it may
+            be treated differently by pyglet. For example, being omitted from text color shaders.
 
     """
-
+    baseline = 0
+    lsb = 0
     advance = 0
     vertices = (0, 0, 0, 0)
+    colored = False
 
-    def set_bearings(self, baseline, left_side_bearing, advance):
+    def set_bearings(self, baseline, left_side_bearing, advance, x_offset=0, y_offset=0):
         """Set metrics for this glyph.
 
         :Parameters:
@@ -165,14 +170,20 @@ class Glyph(image.TextureRegion):
                 Distance to add to the left edge of the glyph.
             `advance` : int
                 Distance to move the horizontal advance to the next glyph.
-
+            `offset_x` : int
+                Distance to move the glyph horizontally from it's default position.
+            `offset_y` : int
+                Distance to move the glyph vertically from it's default position.
         """
+        self.baseline = baseline
+        self.lsb = left_side_bearing
         self.advance = advance
+
         self.vertices = (
-            left_side_bearing,
-            -baseline,
-            left_side_bearing + self.width,
-            -baseline + self.height)
+            left_side_bearing + x_offset,
+            -baseline + y_offset,
+            left_side_bearing + self.width + x_offset,
+            -baseline + self.height + y_offset)
 
     def draw(self):
         """Debug method.
@@ -296,6 +307,11 @@ class Font:
         self.textures = []
         self.glyphs = {}
 
+    @property
+    def name(self):
+        """Return the Family Name of the font as a string."""
+        raise NotImplementedError
+
     @classmethod
     def add_font_data(cls, data):
         """Add font data to the font loader.
@@ -387,7 +403,6 @@ class Font:
             glyphs.append(self.glyphs[c])
         return glyphs
 
-
     def get_glyphs_for_width(self, text, width):
         """Return a list of glyphs for `text` that fit within the given width.
         
diff --git a/pyglet/font/directwrite.py b/pyglet/font/directwrite.py
new file mode 100644
index 0000000..631b49a
--- /dev/null
+++ b/pyglet/font/directwrite.py
@@ -0,0 +1,2365 @@
+import copy
+
+import pyglet
+from pyglet.font import base
+from pyglet.util import debug_print
+from pyglet.image.codecs.wic import IWICBitmap, GUID_WICPixelFormat32bppBGR, WICDecoder, GUID_WICPixelFormat32bppBGRA, \
+    GUID_WICPixelFormat32bppPBGRA
+
+from pyglet import image
+import ctypes
+import math
+from pyglet import com
+from pyglet.libs.win32 import _kernel32 as kernel32
+from pyglet.libs.win32 import _ole32 as ole32
+from pyglet.libs.win32.constants import *
+from pyglet.libs.win32.types import *
+from ctypes import *
+import os
+import platform
+
+try:
+    dwrite = 'dwrite'
+
+    # System32 and SysWOW64 folders are opposite perception in Windows x64.
+    # System32 = x64 dll's | SysWOW64 = x86 dlls
+    # By default ctypes only seems to look in system32 regardless of Python architecture, which has x64 dlls.
+    if platform.architecture()[0] == '32bit':
+        if platform.machine().endswith('64'):  # Machine is 64 bit, Python is 32 bit.
+            dwrite = os.path.join(os.environ['WINDIR'], 'SysWOW64', 'dwrite.dll')
+
+    dwrite_lib = ctypes.windll.LoadLibrary(dwrite)
+except OSError as err:
+    # Doesn't exist? Should stop import of library.
+    pass
+
+_debug_font = debug_print('debug_font')
+
+def DWRITE_MAKE_OPENTYPE_TAG(a, b, c, d):
+    return ord(d) << 24 | ord(c) << 16 | ord(b) << 8 | ord(a)
+
+
+DWRITE_FACTORY_TYPE = UINT
+DWRITE_FACTORY_TYPE_SHARED = 0
+DWRITE_FACTORY_TYPE_ISOLATED = 1
+
+DWRITE_FONT_WEIGHT = UINT
+DWRITE_FONT_WEIGHT_THIN = 100
+DWRITE_FONT_WEIGHT_EXTRA_LIGHT = 200
+DWRITE_FONT_WEIGHT_ULTRA_LIGHT = 200
+DWRITE_FONT_WEIGHT_LIGHT = 300
+DWRITE_FONT_WEIGHT_SEMI_LIGHT = 350
+DWRITE_FONT_WEIGHT_NORMAL = 400
+DWRITE_FONT_WEIGHT_REGULAR = 400
+DWRITE_FONT_WEIGHT_MEDIUM = 500
+DWRITE_FONT_WEIGHT_DEMI_BOLD = 600
+DWRITE_FONT_WEIGHT_SEMI_BOLD = 600
+DWRITE_FONT_WEIGHT_BOLD = 700
+DWRITE_FONT_WEIGHT_EXTRA_BOLD = 800
+DWRITE_FONT_WEIGHT_ULTRA_BOLD = 800
+DWRITE_FONT_WEIGHT_BLACK = 900
+DWRITE_FONT_WEIGHT_HEAVY = 900
+DWRITE_FONT_WEIGHT_EXTRA_BLACK = 950
+
+name_to_weight = {"thin": DWRITE_FONT_WEIGHT_THIN,
+                  "extralight": DWRITE_FONT_WEIGHT_EXTRA_LIGHT,
+                  "ultralight": DWRITE_FONT_WEIGHT_ULTRA_LIGHT,
+                  "light": DWRITE_FONT_WEIGHT_LIGHT,
+                  "semilight": DWRITE_FONT_WEIGHT_SEMI_LIGHT,
+                  "normal": DWRITE_FONT_WEIGHT_NORMAL,
+                  "regular": DWRITE_FONT_WEIGHT_REGULAR,
+                  "medium": DWRITE_FONT_WEIGHT_MEDIUM,
+                  "demibold": DWRITE_FONT_WEIGHT_DEMI_BOLD,
+                  "semibold": DWRITE_FONT_WEIGHT_SEMI_BOLD,
+                  "bold": DWRITE_FONT_WEIGHT_BOLD,
+                  "extrabold": DWRITE_FONT_WEIGHT_EXTRA_BOLD,
+                  "ultrabold": DWRITE_FONT_WEIGHT_ULTRA_BOLD,
+                  "black": DWRITE_FONT_WEIGHT_BLACK,
+                  "heavy": DWRITE_FONT_WEIGHT_HEAVY,
+                  "extrablack": DWRITE_FONT_WEIGHT_EXTRA_BLACK,
+                  }
+
+DWRITE_FONT_STRETCH = UINT
+DWRITE_FONT_STRETCH_UNDEFINED = 0
+DWRITE_FONT_STRETCH_ULTRA_CONDENSED = 1
+DWRITE_FONT_STRETCH_EXTRA_CONDENSED = 2
+DWRITE_FONT_STRETCH_CONDENSED = 3
+DWRITE_FONT_STRETCH_SEMI_CONDENSED = 4
+DWRITE_FONT_STRETCH_NORMAL = 5
+DWRITE_FONT_STRETCH_MEDIUM = 5
+DWRITE_FONT_STRETCH_SEMI_EXPANDED = 6
+DWRITE_FONT_STRETCH_EXPANDED = 7
+DWRITE_FONT_STRETCH_EXTRA_EXPANDED = 8
+
+name_to_stretch = {"undefined": DWRITE_FONT_STRETCH_UNDEFINED,
+                   "ultracondensed": DWRITE_FONT_STRETCH_ULTRA_CONDENSED,
+                   "extracondensed": DWRITE_FONT_STRETCH_EXTRA_CONDENSED,
+                   "condensed": DWRITE_FONT_STRETCH_CONDENSED,
+                   "semicondensed": DWRITE_FONT_STRETCH_SEMI_CONDENSED,
+                   "normal": DWRITE_FONT_STRETCH_NORMAL,
+                   "medium": DWRITE_FONT_STRETCH_MEDIUM,
+                   "semiexpanded": DWRITE_FONT_STRETCH_SEMI_EXPANDED,
+                   "expanded": DWRITE_FONT_STRETCH_EXPANDED,
+                   "extraexpanded": DWRITE_FONT_STRETCH_EXTRA_EXPANDED,
+                   }
+
+DWRITE_GLYPH_IMAGE_FORMATS = c_int
+
+DWRITE_GLYPH_IMAGE_FORMATS_NONE = 0x00000000
+DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE = 0x00000001
+DWRITE_GLYPH_IMAGE_FORMATS_CFF = 0x00000002
+DWRITE_GLYPH_IMAGE_FORMATS_COLR = 0x00000004
+DWRITE_GLYPH_IMAGE_FORMATS_SVG = 0x00000008
+DWRITE_GLYPH_IMAGE_FORMATS_PNG = 0x00000010
+DWRITE_GLYPH_IMAGE_FORMATS_JPEG = 0x00000020
+DWRITE_GLYPH_IMAGE_FORMATS_TIFF = 0x00000040
+DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8 = 0x00000080
+
+DWRITE_MEASURING_MODE = UINT
+DWRITE_MEASURING_MODE_NATURAL = 0
+DWRITE_MEASURING_MODE_GDI_CLASSIC = 1
+DWRITE_MEASURING_MODE_GDI_NATURAL = 2
+
+DWRITE_GLYPH_IMAGE_FORMATS_ALL = DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE | \
+                                    DWRITE_GLYPH_IMAGE_FORMATS_CFF | \
+                                    DWRITE_GLYPH_IMAGE_FORMATS_COLR | \
+                                    DWRITE_GLYPH_IMAGE_FORMATS_SVG | \
+                                    DWRITE_GLYPH_IMAGE_FORMATS_PNG | \
+                                    DWRITE_GLYPH_IMAGE_FORMATS_JPEG | \
+                                    DWRITE_GLYPH_IMAGE_FORMATS_TIFF | \
+                                    DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8
+
+DWRITE_FONT_STYLE = UINT
+DWRITE_FONT_STYLE_NORMAL = 0
+DWRITE_FONT_STYLE_OBLIQUE = 1
+DWRITE_FONT_STYLE_ITALIC = 2
+
+name_to_style = {"normal": DWRITE_FONT_STYLE_NORMAL,
+                 "oblique": DWRITE_FONT_STYLE_OBLIQUE,
+                 "italic": DWRITE_FONT_STYLE_ITALIC}
+
+UINT8 = c_uint8
+UINT16 = c_uint16
+INT16 = c_int16
+INT32 = c_int32
+UINT32 = c_uint32
+UINT64 = c_uint64
+
+
+class D2D_POINT_2F(Structure):
+    _fields_ = (
+        ('x', FLOAT),
+        ('y', FLOAT),
+    )
+
+
+class D2D1_RECT_F(Structure):
+    _fields_ = (
+        ('left', FLOAT),
+        ('top', FLOAT),
+        ('right', FLOAT),
+        ('bottom', FLOAT),
+    )
+
+
+class D2D1_COLOR_F(Structure):
+    _fields_ = (
+        ('r', FLOAT),
+        ('g', FLOAT),
+        ('b', FLOAT),
+        ('a', FLOAT),
+    )
+
+
+
+
+class DWRITE_TEXT_METRICS(ctypes.Structure):
+    _fields_ = (
+        ('left', FLOAT),
+        ('top', FLOAT),
+        ('width', FLOAT),
+        ('widthIncludingTrailingWhitespace', FLOAT),
+        ('height', FLOAT),
+        ('layoutWidth', FLOAT),
+        ('layoutHeight', FLOAT),
+        ('maxBidiReorderingDepth', UINT32),
+        ('lineCount', UINT32),
+    )
+
+
+class DWRITE_FONT_METRICS(ctypes.Structure):
+    _fields_ = (
+        ('designUnitsPerEm', UINT16),
+        ('ascent', UINT16),
+        ('descent', UINT16),
+        ('lineGap', INT16),
+        ('capHeight', UINT16),
+        ('xHeight', UINT16),
+        ('underlinePosition', INT16),
+        ('underlineThickness', UINT16),
+        ('strikethroughPosition', INT16),
+        ('strikethroughThickness', UINT16),
+    )
+
+
+class DWRITE_GLYPH_METRICS(ctypes.Structure):
+    _fields_ = (
+        ('leftSideBearing', INT32),
+        ('advanceWidth', UINT32),
+        ('rightSideBearing', INT32),
+        ('topSideBearing', INT32),
+        ('advanceHeight', UINT32),
+        ('bottomSideBearing', INT32),
+        ('verticalOriginY', INT32),
+    )
+
+
+class DWRITE_GLYPH_OFFSET(ctypes.Structure):
+    _fields_ = (
+        ('advanceOffset', FLOAT),
+        ('ascenderOffset', FLOAT),
+    )
+
+    def __repr__(self):
+        return f"DWRITE_GLYPH_OFFSET({self.advanceOffset}, {self.ascenderOffset})"
+
+
+class DWRITE_CLUSTER_METRICS(ctypes.Structure):
+    _fields_ = (
+        ('width', FLOAT),
+        ('length', UINT16),
+        ('canWrapLineAfter', UINT16, 1),
+        ('isWhitespace', UINT16, 1),
+        ('isNewline', UINT16, 1),
+        ('isSoftHyphen', UINT16, 1),
+        ('isRightToLeft', UINT16, 1),
+        ('padding', UINT16, 11),
+    )
+
+
+class IDWriteFontFace(com.pIUnknown):
+    _methods_ = [
+        ('GetType',
+         com.STDMETHOD()),
+        ('GetFiles',
+         com.STDMETHOD()),
+        ('GetIndex',
+         com.STDMETHOD()),
+        ('GetSimulations',
+         com.STDMETHOD()),
+        ('IsSymbolFont',
+         com.STDMETHOD()),
+        ('GetMetrics',
+         com.METHOD(c_void, POINTER(DWRITE_FONT_METRICS))),
+        ('GetGlyphCount',
+         com.METHOD(UINT16)),
+        ('GetDesignGlyphMetrics',
+         com.STDMETHOD(POINTER(UINT16), UINT32, POINTER(DWRITE_GLYPH_METRICS), BOOL)),
+        ('GetGlyphIndices',
+         com.STDMETHOD(POINTER(UINT32), UINT32, POINTER(UINT16))),
+        ('TryGetFontTable',
+         com.STDMETHOD(UINT32, c_void_p, POINTER(UINT32), c_void_p, POINTER(BOOL))),
+        ('ReleaseFontTable',
+         com.METHOD(c_void)),
+        ('GetGlyphRunOutline',
+         com.STDMETHOD()),
+        ('GetRecommendedRenderingMode',
+         com.STDMETHOD()),
+        ('GetGdiCompatibleMetrics',
+         com.STDMETHOD()),
+        ('GetGdiCompatibleGlyphMetrics',
+         com.STDMETHOD()),
+    ]
+
+
+IID_IDWriteFontFace1 = com.GUID(0xa71efdb4, 0x9fdb, 0x4838, 0xad, 0x90, 0xcf, 0xc3, 0xbe, 0x8c, 0x3d, 0xaf)
+
+
+class IDWriteFontFace1(IDWriteFontFace, com.pIUnknown):
+    _methods_ = [
+        ('GetMetric1',
+         com.STDMETHOD()),
+        ('GetGdiCompatibleMetrics1',
+         com.STDMETHOD()),
+        ('GetCaretMetrics',
+         com.STDMETHOD()),
+        ('GetUnicodeRanges',
+         com.STDMETHOD()),
+        ('IsMonospacedFont',
+         com.STDMETHOD()),
+        ('GetDesignGlyphAdvances',
+         com.METHOD(c_void, POINTER(DWRITE_FONT_METRICS))),
+        ('GetGdiCompatibleGlyphAdvances',
+         com.STDMETHOD()),
+        ('GetKerningPairAdjustments',
+         com.STDMETHOD(UINT32, POINTER(UINT16), POINTER(INT32))),
+        ('HasKerningPairs',
+         com.METHOD(BOOL)),
+        ('GetRecommendedRenderingMode1',
+         com.STDMETHOD()),
+        ('GetVerticalGlyphVariants',
+         com.STDMETHOD()),
+        ('HasVerticalGlyphVariants',
+         com.STDMETHOD())
+    ]
+
+
+class DWRITE_GLYPH_RUN(ctypes.Structure):
+    _fields_ = (
+        ('fontFace', IDWriteFontFace),
+        ('fontEmSize', FLOAT),
+        ('glyphCount', UINT32),
+        ('glyphIndices', POINTER(UINT16)),
+        ('glyphAdvances', POINTER(FLOAT)),
+        ('glyphOffsets', POINTER(DWRITE_GLYPH_OFFSET)),
+        ('isSideways', BOOL),
+        ('bidiLevel', UINT32),
+    )
+
+DWRITE_SCRIPT_SHAPES = UINT
+DWRITE_SCRIPT_SHAPES_DEFAULT = 0
+
+
+class DWRITE_SCRIPT_ANALYSIS(ctypes.Structure):
+    _fields_ = (
+        ('script', UINT16),
+        ('shapes', DWRITE_SCRIPT_SHAPES),
+    )
+
+
+DWRITE_FONT_FEATURE_TAG = UINT
+
+
+class DWRITE_FONT_FEATURE(ctypes.Structure):
+    _fields_ = (
+        ('nameTag', DWRITE_FONT_FEATURE_TAG),
+        ('parameter', UINT32),
+    )
+
+
+class DWRITE_TYPOGRAPHIC_FEATURES(ctypes.Structure):
+    _fields_ = (
+        ('features', POINTER(DWRITE_FONT_FEATURE)),
+        ('featureCount', UINT32),
+    )
+
+
+class DWRITE_SHAPING_TEXT_PROPERTIES(ctypes.Structure):
+    _fields_ = (
+        ('isShapedAlone', UINT16, 1),
+        ('reserved1', UINT16, 1),
+        ('canBreakShapingAfter', UINT16, 1),
+        ('reserved', UINT16, 13),
+    )
+
+    def __repr__(self):
+        return f"DWRITE_SHAPING_TEXT_PROPERTIES({self.isShapedAlone}, {self.reserved1}, {self.canBreakShapingAfter})"
+
+
+class DWRITE_SHAPING_GLYPH_PROPERTIES(ctypes.Structure):
+    _fields_ = (
+        ('justification', UINT16, 4),
+        ('isClusterStart', UINT16, 1),
+        ('isDiacritic', UINT16, 1),
+        ('isZeroWidthSpace', UINT16, 1),
+        ('reserved', UINT16, 9),
+    )
+
+
+DWRITE_READING_DIRECTION = UINT
+DWRITE_READING_DIRECTION_LEFT_TO_RIGHT = 0
+
+
+class IDWriteTextAnalysisSource(com.IUnknown):
+    _methods_ = [
+        ('GetTextAtPosition',
+         com.METHOD(HRESULT, c_void_p, UINT32, POINTER(c_wchar_p), POINTER(UINT32))),
+        ('GetTextBeforePosition',
+         com.STDMETHOD(UINT32, c_wchar_p, POINTER(UINT32))),
+        ('GetParagraphReadingDirection',
+         com.METHOD(DWRITE_READING_DIRECTION)),
+        ('GetLocaleName',
+         com.STDMETHOD(c_void_p, UINT32, POINTER(UINT32), POINTER(c_wchar_p))),
+        ('GetNumberSubstitution',
+         com.STDMETHOD(UINT32, POINTER(UINT32), c_void_p)),
+    ]
+
+
+class IDWriteTextAnalysisSink(com.IUnknown):
+    _methods_ = [
+        ('SetScriptAnalysis',
+         com.STDMETHOD(c_void_p, UINT32, UINT32, POINTER(DWRITE_SCRIPT_ANALYSIS))),
+        ('SetLineBreakpoints',
+         com.STDMETHOD(UINT32, UINT32, c_void_p)),
+        ('SetBidiLevel',
+         com.STDMETHOD(UINT32, UINT32, UINT8, UINT8)),
+        ('SetNumberSubstitution',
+         com.STDMETHOD(UINT32, UINT32, c_void_p)),
+    ]
+
+
+class Run:
+    def __init__(self):
+        self.text_start = 0
+        self.text_length = 0
+        self.glyph_start = 0
+        self.glyph_count = 0
+        self.script = DWRITE_SCRIPT_ANALYSIS()
+        self.bidi = 0
+        self.isNumberSubstituted = False
+        self.isSideways = False
+
+        self.next_run = None
+
+    def ContainsTextPosition(self, textPosition):
+        return textPosition >= self.text_start and textPosition < self.text_start + self.text_length
+
+
+class TextAnalysis(com.COMObject):
+    _interfaces_ = [IDWriteTextAnalysisSource, IDWriteTextAnalysisSink]
+
+    def __init__(self):
+        super().__init__()
+        self._textstart = 0
+        self._textlength = 0
+        self._glyphstart = 0
+        self._glyphcount = 0
+        self._ptrs = []
+
+        self._script = None
+        self._bidi = 0
+        # self._sideways = False
+
+    def GenerateResults(self, analyzer, text, text_length):
+        self._text = text
+        self._textstart = 0
+        self._textlength = text_length
+        self._glyphstart = 0
+        self._glyphcount = 0
+        self._ptrs.clear()
+
+        self._start_run = Run()
+        self._start_run.text_length = text_length
+
+        self._current_run = self._start_run
+
+        analyzer.AnalyzeScript(self, 0, text_length, self)
+
+    def SetScriptAnalysis(self, this, textPosition, textLength, scriptAnalysis):
+        # textPosition - The index of the first character in the string that the result applies to
+        # textLength - How many characters of the string from the index that the result applies to
+        # scriptAnalysis - The analysis information for all glyphs starting at position for length.
+        self.SetCurrentRun(textPosition)
+        self.SplitCurrentRun(textPosition)
+
+        while textLength > 0:
+            run, textLength = self.FetchNextRun(textLength)
+
+            run.script.script = scriptAnalysis[0].script
+            run.script.shapes = scriptAnalysis[0].shapes
+
+            self._script = run.script
+
+        return 0
+        # return 0x80004001
+
+    def GetTextBeforePosition(self, this, textPosition, textString, textLength):
+        raise Exception("Currently not implemented.")
+
+    def GetTextAtPosition(self, this, textPosition, textString, textLength):
+        # This method will retrieve a substring of the text in this layout
+        #   to be used in an analysis step.
+        # Arguments:
+        # textPosition - The index of the first character of the text to retrieve.
+        # textString - The pointer to the first character of text at the index requested.
+        # textLength - The characters available at/after the textString pointer (string length).
+
+        if textPosition >= self._textlength:
+            self._no_ptr = c_wchar_p(None)
+            textString[0] = self._no_ptr
+            textLength[0] = 0
+        else:
+            ptr = c_wchar_p(self._text[textPosition:])
+            self._ptrs.append(ptr)
+            textString[0] = ptr
+            textLength[0] = self._textlength - textPosition
+
+        return 0
+
+    def GetParagraphReadingDirection(self):
+        return 0
+
+    def GetLocaleName(self, this, textPosition, textLength, localeName):
+        self.__local_name = c_wchar_p("")  # TODO: Add more locales.
+        localeName[0] = self.__local_name
+        textLength[0] = self._textlength - textPosition
+        return 0
+
+    def GetNumberSubstitution(self):
+        return 0
+
+    def SetCurrentRun(self, textPosition):
+        if self._current_run and self._current_run.ContainsTextPosition(textPosition):
+            return
+
+    def SplitCurrentRun(self, textPosition):
+        if not self._current_run:
+            return
+
+        if textPosition <= self._current_run.text_start:
+            # Already first start of the run.
+            return
+
+        new_run = copy.copy(self._current_run)
+
+        new_run.next_run = self._current_run.next_run
+        self._current_run.next_run = new_run
+
+        splitPoint = textPosition - self._current_run.text_start
+        new_run.text_start += splitPoint
+        new_run.text_length -= splitPoint
+
+        self._current_run.text_length = splitPoint
+        self._current_run = new_run
+
+    def FetchNextRun(self, textLength):
+        original_run = self._current_run
+
+        if (textLength < self._current_run.text_length):
+            self.SplitCurrentRun(self._current_run.text_start + textLength)
+        else:
+            self._current_run = self._current_run.next_run
+
+        textLength -= original_run.text_length
+
+        return original_run, textLength
+
+
+class IDWriteTextAnalyzer(com.pIUnknown):
+    _methods_ = [
+        ('AnalyzeScript',
+         com.STDMETHOD(POINTER(IDWriteTextAnalysisSource), UINT32, UINT32, POINTER(IDWriteTextAnalysisSink))),
+        ('AnalyzeBidi',
+         com.STDMETHOD()),
+        ('AnalyzeNumberSubstitution',
+         com.STDMETHOD()),
+        ('AnalyzeLineBreakpoints',
+         com.STDMETHOD()),
+        ('GetGlyphs',
+         com.STDMETHOD(c_wchar_p, UINT32, IDWriteFontFace, BOOL, BOOL, POINTER(DWRITE_SCRIPT_ANALYSIS),
+                       c_wchar_p, c_void_p, POINTER(POINTER(DWRITE_TYPOGRAPHIC_FEATURES)), POINTER(UINT32),
+                       UINT32, UINT32, POINTER(UINT16), POINTER(DWRITE_SHAPING_TEXT_PROPERTIES),
+                       POINTER(UINT16), POINTER(DWRITE_SHAPING_GLYPH_PROPERTIES), POINTER(UINT32))),
+        ('GetGlyphPlacements',
+         com.STDMETHOD(c_wchar_p, POINTER(UINT16), POINTER(DWRITE_SHAPING_TEXT_PROPERTIES), UINT32, POINTER(UINT16),
+                       POINTER(DWRITE_SHAPING_GLYPH_PROPERTIES), UINT32, IDWriteFontFace, FLOAT, BOOL, BOOL,
+                       POINTER(DWRITE_SCRIPT_ANALYSIS), c_wchar_p, POINTER(DWRITE_TYPOGRAPHIC_FEATURES),
+                       POINTER(UINT32), UINT32, POINTER(FLOAT), POINTER(DWRITE_GLYPH_OFFSET))),
+        ('GetGdiCompatibleGlyphPlacements',
+         com.STDMETHOD()),
+    ]
+
+
+class IDWriteLocalizedStrings(com.pIUnknown):
+    _methods_ = [
+        ('GetCount',
+         com.METHOD(UINT32)),
+        ('FindLocaleName',
+         com.STDMETHOD(c_wchar_p, POINTER(UINT32), POINTER(BOOL))),
+        ('GetLocaleNameLength',
+         com.STDMETHOD(UINT32, POINTER(UINT32))),
+        ('GetLocaleName',
+         com.STDMETHOD(UINT32, c_wchar_p, UINT32)),
+        ('GetStringLength',
+         com.STDMETHOD(UINT32, POINTER(UINT32))),
+        ('GetString',
+         com.STDMETHOD(UINT32, c_wchar_p, UINT32)),
+    ]
+
+
+class IDWriteFontList(com.pIUnknown):
+    _methods_ = [
+        ('GetFontCollection',
+         com.STDMETHOD()),
+        ('GetFontCount',
+         com.STDMETHOD()),
+        ('GetFont',
+         com.STDMETHOD()),
+    ]
+
+
+class IDWriteFontFamily(IDWriteFontList, com.pIUnknown):
+    _methods_ = [
+        ('GetFamilyNames',
+         com.STDMETHOD(POINTER(IDWriteLocalizedStrings))),
+        ('GetFirstMatchingFont',
+         com.STDMETHOD(DWRITE_FONT_WEIGHT, DWRITE_FONT_STRETCH, DWRITE_FONT_STYLE, c_void_p)),
+        ('GetMatchingFonts',
+         com.STDMETHOD()),
+    ]
+
+
+class IDWriteFontFamily1(IDWriteFontFamily, IDWriteFontList, com.pIUnknown):
+    _methods_ = [
+        ('GetFontLocality',
+         com.STDMETHOD()),
+        ('GetFont1',
+         com.STDMETHOD()),
+        ('GetFontFaceReference',
+         com.STDMETHOD()),
+    ]
+
+
+class IDWriteFontFile(com.pIUnknown):
+    _methods_ = [
+        ('GetReferenceKey',
+         com.STDMETHOD()),
+        ('GetLoader',
+         com.STDMETHOD()),
+        ('Analyze',
+         com.STDMETHOD()),
+    ]
+
+
+class IDWriteFont(com.pIUnknown):
+    _methods_ = [
+        ('GetFontFamily',
+         com.STDMETHOD(POINTER(IDWriteFontFamily))),
+        ('GetWeight',
+         com.STDMETHOD()),
+        ('GetStretch',
+         com.STDMETHOD()),
+        ('GetStyle',
+         com.STDMETHOD()),
+        ('IsSymbolFont',
+         com.STDMETHOD()),
+        ('GetFaceNames',
+         com.STDMETHOD(POINTER(IDWriteLocalizedStrings))),
+        ('GetInformationalStrings',
+         com.STDMETHOD()),
+        ('GetSimulations',
+         com.STDMETHOD()),
+        ('GetMetrics',
+         com.STDMETHOD()),
+        ('HasCharacter',
+         com.STDMETHOD(UINT32, POINTER(BOOL))),
+        ('CreateFontFace',
+         com.STDMETHOD(POINTER(IDWriteFontFace))),
+    ]
+
+
+class IDWriteFont1(IDWriteFont, com.pIUnknown):
+    _methods_ = [
+        ('GetMetrics1',
+         com.STDMETHOD()),
+        ('GetPanose',
+         com.STDMETHOD()),
+        ('GetUnicodeRanges',
+         com.STDMETHOD()),
+        ('IsMonospacedFont',
+         com.STDMETHOD())
+    ]
+
+
+class IDWriteFontCollection(com.pIUnknown):
+    _methods_ = [
+        ('GetFontFamilyCount',
+         com.STDMETHOD()),
+        ('GetFontFamily',
+         com.STDMETHOD(UINT32, POINTER(IDWriteFontFamily))),
+        ('FindFamilyName',
+         com.STDMETHOD(c_wchar_p, POINTER(UINT), POINTER(BOOL))),
+        ('GetFontFromFontFace',
+         com.STDMETHOD()),
+    ]
+
+
+class IDWriteFontCollection1(IDWriteFontCollection, com.pIUnknown):
+    _methods_ = [
+        ('GetFontSet',
+         com.STDMETHOD()),
+        ('GetFontFamily1',
+         com.STDMETHOD(POINTER(IDWriteFontFamily1))),
+    ]
+
+
+DWRITE_TEXT_ALIGNMENT = UINT
+DWRITE_TEXT_ALIGNMENT_LEADING = 1
+DWRITE_TEXT_ALIGNMENT_TRAILING = 2
+DWRITE_TEXT_ALIGNMENT_CENTER = 3
+DWRITE_TEXT_ALIGNMENT_JUSTIFIED = 4
+
+
+class IDWriteTextFormat(com.pIUnknown):
+    _methods_ = [
+        ('SetTextAlignment',
+         com.STDMETHOD(DWRITE_TEXT_ALIGNMENT)),
+        ('SetParagraphAlignment',
+         com.STDMETHOD()),
+        ('SetWordWrapping',
+         com.STDMETHOD()),
+        ('SetReadingDirection',
+         com.STDMETHOD()),
+        ('SetFlowDirection',
+         com.STDMETHOD()),
+        ('SetIncrementalTabStop',
+         com.STDMETHOD()),
+        ('SetTrimming',
+         com.STDMETHOD()),
+        ('SetLineSpacing',
+         com.STDMETHOD()),
+        ('GetTextAlignment',
+         com.STDMETHOD()),
+        ('GetParagraphAlignment',
+         com.STDMETHOD()),
+        ('GetWordWrapping',
+         com.STDMETHOD()),
+        ('GetReadingDirection',
+         com.STDMETHOD()),
+        ('GetFlowDirection',
+         com.STDMETHOD()),
+        ('GetIncrementalTabStop',
+         com.STDMETHOD()),
+        ('GetTrimming',
+         com.STDMETHOD()),
+        ('GetLineSpacing',
+         com.STDMETHOD()),
+        ('GetFontCollection',
+         com.STDMETHOD()),
+        ('GetFontFamilyNameLength',
+         com.STDMETHOD(UINT32, POINTER(UINT32))),
+        ('GetFontFamilyName',
+         com.STDMETHOD(UINT32, c_wchar_p, UINT32)),
+        ('GetFontWeight',
+         com.STDMETHOD()),
+        ('GetFontStyle',
+         com.STDMETHOD()),
+        ('GetFontStretch',
+         com.STDMETHOD()),
+        ('GetFontSize',
+         com.STDMETHOD()),
+        ('GetLocaleNameLength',
+         com.STDMETHOD()),
+        ('GetLocaleName',
+         com.STDMETHOD()),
+    ]
+
+
+class IDWriteTypography(com.pIUnknown):
+    _methods_ = [
+        ('AddFontFeature',
+         com.STDMETHOD(DWRITE_FONT_FEATURE)),
+        ('GetFontFeatureCount',
+         com.METHOD(UINT32)),
+        ('GetFontFeature',
+         com.STDMETHOD())
+    ]
+
+
+class DWRITE_TEXT_RANGE(ctypes.Structure):
+    _fields_ = (
+        ('startPosition', UINT32),
+        ('length', UINT32),
+    )
+
+
+class DWRITE_OVERHANG_METRICS(ctypes.Structure):
+    _fields_ = (
+        ('left', FLOAT),
+        ('top', FLOAT),
+        ('right', FLOAT),
+        ('bottom', FLOAT),
+    )
+
+
+class IDWriteTextLayout(IDWriteTextFormat, com.pIUnknown):
+    _methods_ = [
+        ('SetMaxWidth',
+         com.STDMETHOD()),
+        ('SetMaxHeight',
+         com.STDMETHOD()),
+        ('SetFontCollection',
+         com.STDMETHOD()),
+        ('SetFontFamilyName',
+         com.STDMETHOD()),
+        ('SetFontWeight',  # 30
+         com.STDMETHOD()),
+        ('SetFontStyle',
+         com.STDMETHOD()),
+        ('SetFontStretch',
+         com.STDMETHOD()),
+        ('SetFontSize',
+         com.STDMETHOD()),
+        ('SetUnderline',
+         com.STDMETHOD()),
+        ('SetStrikethrough',
+         com.STDMETHOD()),
+        ('SetDrawingEffect',
+         com.STDMETHOD()),
+        ('SetInlineObject',
+         com.STDMETHOD()),
+        ('SetTypography',
+         com.STDMETHOD(IDWriteTypography, DWRITE_TEXT_RANGE)),
+        ('SetLocaleName',
+         com.STDMETHOD()),
+        ('GetMaxWidth',  # 40
+         com.METHOD(FLOAT)),
+        ('GetMaxHeight',
+         com.METHOD(FLOAT)),
+        ('GetFontCollection2',
+         com.STDMETHOD()),
+        ('GetFontFamilyNameLength2',
+         com.STDMETHOD(UINT32, POINTER(UINT32), c_void_p)),
+        ('GetFontFamilyName2',
+         com.STDMETHOD(UINT32, c_wchar_p, UINT32, c_void_p)),
+        ('GetFontWeight2',
+         com.STDMETHOD(UINT32, POINTER(DWRITE_FONT_WEIGHT), POINTER(DWRITE_TEXT_RANGE))),
+        ('GetFontStyle2',
+         com.STDMETHOD()),
+        ('GetFontStretch2',
+         com.STDMETHOD()),
+        ('GetFontSize2',
+         com.STDMETHOD()),
+        ('GetUnderline',
+         com.STDMETHOD()),
+        ('GetStrikethrough',
+         com.STDMETHOD(UINT32, POINTER(BOOL), POINTER(DWRITE_TEXT_RANGE))),
+        ('GetDrawingEffect',
+         com.STDMETHOD()),
+        ('GetInlineObject',
+         com.STDMETHOD()),
+        ('GetTypography',  # Always returns NULL without SetTypography being called.
+         com.STDMETHOD(UINT32, POINTER(IDWriteTypography), POINTER(DWRITE_TEXT_RANGE))),
+        ('GetLocaleNameLength1',
+         com.STDMETHOD()),
+        ('GetLocaleName1',
+         com.STDMETHOD()),
+        ('Draw',
+         com.STDMETHOD()),
+        ('GetLineMetrics',
+         com.STDMETHOD()),
+        ('GetMetrics',
+         com.STDMETHOD(POINTER(DWRITE_TEXT_METRICS))),
+        ('GetOverhangMetrics',
+         com.STDMETHOD(POINTER(DWRITE_OVERHANG_METRICS))),
+        ('GetClusterMetrics',
+         com.STDMETHOD(POINTER(DWRITE_CLUSTER_METRICS), UINT32, POINTER(UINT32))),
+        ('DetermineMinWidth',
+         com.STDMETHOD(POINTER(FLOAT))),
+        ('HitTestPoint',
+         com.STDMETHOD()),
+        ('HitTestTextPosition',
+         com.STDMETHOD()),
+        ('HitTestTextRange',
+         com.STDMETHOD()),
+    ]
+
+
+class IDWriteTextLayout1(IDWriteTextLayout, IDWriteTextFormat, com.pIUnknown):
+    _methods_ = [
+        ('SetPairKerning',
+         com.STDMETHOD()),
+        ('GetPairKerning',
+         com.STDMETHOD()),
+        ('SetCharacterSpacing',
+         com.STDMETHOD()),
+        ('GetCharacterSpacing',
+         com.STDMETHOD(UINT32, POINTER(FLOAT), POINTER(FLOAT), POINTER(FLOAT), POINTER(DWRITE_TEXT_RANGE))),
+    ]
+
+
+class IDWriteFontFileEnumerator(com.IUnknown):
+    _methods_ = [
+        ('MoveNext',
+         com.STDMETHOD(c_void_p, POINTER(BOOL))),
+        ('GetCurrentFontFile',
+         com.STDMETHOD(c_void_p, c_void_p)),
+    ]
+
+
+class IDWriteFontCollectionLoader(com.IUnknown):
+    _methods_ = [
+        ('CreateEnumeratorFromKey',
+         com.STDMETHOD(c_void_p, c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileEnumerator)))),
+    ]
+
+
+class IDWriteFontFileStream(com.IUnknown):
+    _methods_ = [
+        ('ReadFileFragment',
+         com.STDMETHOD(c_void_p, POINTER(c_void_p), UINT64, UINT64, POINTER(c_void_p))),
+        ('ReleaseFileFragment',
+         com.STDMETHOD(c_void_p, c_void_p)),
+        ('GetFileSize',
+         com.STDMETHOD(c_void_p, POINTER(UINT64))),
+        ('GetLastWriteTime',
+         com.STDMETHOD(c_void_p, POINTER(UINT64))),
+    ]
+
+
+class MyFontFileStream(com.COMObject):
+    _interfaces_ = [IDWriteFontFileStream]
+
+    def __init__(self, data):
+        self._data = data
+        self._size = len(data)
+        self._ptrs = []
+
+    def AddRef(self, this):
+        return 1
+
+    def Release(self, this):
+        return 1
+
+    def QueryInterface(self, this, refiid, tester):
+        return 0
+
+    def ReadFileFragment(self, this, fragmentStart, fileOffset, fragmentSize, fragmentContext):
+        if fileOffset + fragmentSize > self._size:
+            return 0x80004005  # E_FAIL
+
+        fragment = self._data[fileOffset:]
+        buffer = (ctypes.c_ubyte * len(fragment)).from_buffer(bytearray(fragment))
+        ptr = cast(buffer, c_void_p)
+
+        self._ptrs.append(ptr)
+        fragmentStart[0] = ptr
+        fragmentContext[0] = None
+        return 0
+
+    def ReleaseFileFragment(self, this, fragmentContext):
+        return 0
+
+    def GetFileSize(self, this, fileSize):
+        fileSize[0] = self._size
+        return 0
+
+    def GetLastWriteTime(self, this, lastWriteTime):
+        return 0x80004001  # E_NOTIMPL
+
+
+class IDWriteFontFileLoader(com.IUnknown):
+    _methods_ = [
+        ('CreateStreamFromKey',
+         com.STDMETHOD(c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream))))
+    ]
+
+
+class LegacyFontFileLoader(com.COMObject):
+    _interfaces_ = [IDWriteFontFileLoader]
+
+    def __init__(self):
+        self._streams = {}
+
+    def QueryInterface(self, this, refiid, tester):
+        return 0
+
+    def AddRef(self, this):
+        return 1
+
+    def Release(self, this):
+        return 1
+
+    def CreateStreamFromKey(self, this, fontfileReferenceKey, fontFileReferenceKeySize, fontFileStream):
+        convert_index = cast(fontfileReferenceKey, POINTER(c_uint32))
+
+        self._ptr = ctypes.cast(self._streams[convert_index.contents.value]._pointers[IDWriteFontFileStream],
+                                POINTER(IDWriteFontFileStream))
+        fontFileStream[0] = self._ptr
+        return 0
+
+    def SetCurrentFont(self, index, data):
+        self._streams[index] = MyFontFileStream(data)
+
+
+class MyEnumerator(com.COMObject):
+    _interfaces_ = [IDWriteFontFileEnumerator]
+
+    def __init__(self, factory, loader):
+        self.factory = cast(factory, IDWriteFactory)
+        self.key = "pyglet_dwrite"
+        self.size = len(self.key)
+        self.current_index = -1
+
+        self._keys = []
+        self._font_data = []
+        self._font_files = []
+        self._current_file = None
+
+        self._font_key_ref = create_unicode_buffer("none")
+        self._font_key_len = len(self._font_key_ref)
+
+        self._file_loader = loader
+
+    def AddFontData(self, fonts):
+        self._font_data = fonts
+
+    def MoveNext(self, this, hasCurrentFile):
+
+        self.current_index += 1
+        if self.current_index != len(self._font_data):
+            font_file = IDWriteFontFile()
+
+            self._file_loader.SetCurrentFont(self.current_index, self._font_data[self.current_index])
+
+            key = self.current_index
+
+            if not self.current_index in self._keys:
+                buffer = pointer(c_uint32(key))
+
+                ptr = cast(buffer, c_void_p)
+
+                self._keys.append(ptr)
+
+            self.factory.CreateCustomFontFileReference(self._keys[self.current_index],
+                                                       sizeof(buffer),
+                                                       self._file_loader,
+                                                       byref(font_file))
+
+            self._font_files.append(font_file)
+
+            hasCurrentFile[0] = 1
+        else:
+            hasCurrentFile[0] = 0
+
+        pass
+
+    def GetCurrentFontFile(self, this, fontFile):
+        fontFile = cast(fontFile, POINTER(IDWriteFontFile))
+        fontFile[0] = self._font_files[self.current_index]
+        return 0
+
+
+class LegacyCollectionLoader(com.COMObject):
+    _interfaces_ = [IDWriteFontCollectionLoader]
+
+    def __init__(self, factory, loader):
+        self._enumerator = MyEnumerator(factory, loader)
+
+    def AddFontData(self, fonts):
+        self._enumerator.AddFontData(fonts)
+
+    def AddRef(self, this):
+        self._i = 1
+        return 1
+
+    def Release(self, this):
+        self._i = 0
+        return 1
+
+    def QueryInterface(self, this, refiid, tester):
+        return 0
+
+    def CreateEnumeratorFromKey(self, this, factory, key, key_size, enumerator):
+        self._ptr = ctypes.cast(self._enumerator._pointers[IDWriteFontFileEnumerator],
+                                POINTER(IDWriteFontFileEnumerator))
+
+        enumerator[0] = self._ptr
+        return 0
+
+
+IID_IDWriteFactory = com.GUID(0xb859ee5a, 0xd838, 0x4b5b, 0xa2, 0xe8, 0x1a, 0xdc, 0x7d, 0x93, 0xdb, 0x48)
+
+
+class IDWriteFactory(com.pIUnknown):
+    _methods_ = [
+        ('GetSystemFontCollection',
+         com.STDMETHOD(POINTER(IDWriteFontCollection), BOOL)),
+        ('CreateCustomFontCollection',
+         com.STDMETHOD(POINTER(IDWriteFontCollectionLoader), c_void_p, UINT32, POINTER(IDWriteFontCollection))),
+        ('RegisterFontCollectionLoader',
+         com.STDMETHOD(POINTER(IDWriteFontCollectionLoader))),
+        ('UnregisterFontCollectionLoader',
+         com.STDMETHOD(POINTER(IDWriteFontCollectionLoader))),
+        ('CreateFontFileReference',
+         com.STDMETHOD(c_wchar_p, c_void_p, POINTER(IDWriteFontFile))),
+        ('CreateCustomFontFileReference',
+         com.STDMETHOD(c_void_p, UINT32, POINTER(IDWriteFontFileLoader), POINTER(IDWriteFontFile))),
+        ('CreateFontFace',
+         com.STDMETHOD()),
+        ('CreateRenderingParams',
+         com.STDMETHOD()),
+        ('CreateMonitorRenderingParams',
+         com.STDMETHOD()),
+        ('CreateCustomRenderingParams',
+         com.STDMETHOD()),
+        ('RegisterFontFileLoader',
+         com.STDMETHOD(c_void_p)),  # Ambigious as newer is a pIUnknown and legacy is IUnknown.
+        ('UnregisterFontFileLoader',
+         com.STDMETHOD(POINTER(IDWriteFontFileLoader))),
+        ('CreateTextFormat',
+         com.STDMETHOD(c_wchar_p, IDWriteFontCollection, DWRITE_FONT_WEIGHT, DWRITE_FONT_STYLE, DWRITE_FONT_STRETCH,
+                       FLOAT, c_wchar_p, POINTER(IDWriteTextFormat))),
+        ('CreateTypography',
+         com.STDMETHOD(POINTER(IDWriteTypography))),
+        ('GetGdiInterop',
+         com.STDMETHOD()),
+        ('CreateTextLayout',
+         com.STDMETHOD(c_wchar_p, UINT32, IDWriteTextFormat, FLOAT, FLOAT, POINTER(IDWriteTextLayout))),
+        ('CreateGdiCompatibleTextLayout',
+         com.STDMETHOD()),
+        ('CreateEllipsisTrimmingSign',
+         com.STDMETHOD()),
+        ('CreateTextAnalyzer',
+         com.STDMETHOD(POINTER(IDWriteTextAnalyzer))),
+        ('CreateNumberSubstitution',
+         com.STDMETHOD()),
+        ('CreateGlyphRunAnalysis',
+         com.STDMETHOD()),
+    ]
+
+
+IID_IDWriteFactory1 = com.GUID(0x30572f99, 0xdac6, 0x41db, 0xa1, 0x6e, 0x04, 0x86, 0x30, 0x7e, 0x60, 0x6a)
+
+
+class IDWriteFactory1(IDWriteFactory, com.pIUnknown):
+    _methods_ = [
+        ('GetEudcFontCollection',
+         com.STDMETHOD()),
+        ('CreateCustomRenderingParams',
+         com.STDMETHOD()),
+    ]
+
+
+class IDWriteFontFallback(com.pIUnknown):
+    _methods_ = [
+        ('MapCharacters',
+         com.STDMETHOD(POINTER(IDWriteTextAnalysisSource), UINT32, UINT32, IDWriteFontCollection, c_wchar_p,
+                       DWRITE_FONT_WEIGHT, DWRITE_FONT_STYLE, DWRITE_FONT_STRETCH, POINTER(UINT32),
+                       POINTER(IDWriteFont),
+                       POINTER(FLOAT))),
+    ]
+
+
+class IDWriteFactory2(IDWriteFactory1, com.pIUnknown):
+    _methods_ = [
+        ('GetSystemFontFallback',
+         com.STDMETHOD(POINTER(IDWriteFontFallback))),
+        ('CreateFontFallbackBuilder',
+         com.STDMETHOD()),
+        ('TranslateColorGlyphRun',
+         com.STDMETHOD()),
+        ('CreateCustomRenderingParams',
+         com.STDMETHOD()),
+        ('CreateGlyphRunAnalysis',
+         com.STDMETHOD()),
+    ]
+
+
+class IDWriteFontSet(com.pIUnknown):
+    _methods_ = [
+        ('GetFontCount',
+         com.STDMETHOD()),
+        ('GetFontFaceReference',
+         com.STDMETHOD()),
+        ('FindFontFaceReference',
+         com.STDMETHOD()),
+        ('FindFontFace',
+         com.STDMETHOD()),
+        ('GetPropertyValues',
+         com.STDMETHOD()),
+        ('GetPropertyOccurrenceCount',
+         com.STDMETHOD()),
+        ('GetMatchingFonts',
+         com.STDMETHOD()),
+        ('GetMatchingFonts',
+         com.STDMETHOD()),
+    ]
+
+
+class IDWriteFontSetBuilder(com.pIUnknown):
+    _methods_ = [
+        ('AddFontFaceReference',
+         com.STDMETHOD()),
+        ('AddFontFaceReference',
+         com.STDMETHOD()),
+        ('AddFontSet',
+         com.STDMETHOD()),
+        ('CreateFontSet',
+         com.STDMETHOD(POINTER(IDWriteFontSet))),
+    ]
+
+
+class IDWriteFontSetBuilder1(IDWriteFontSetBuilder, com.pIUnknown):
+    _methods_ = [
+        ('AddFontFile',
+         com.STDMETHOD(IDWriteFontFile)),
+    ]
+
+
+class IDWriteFactory3(IDWriteFactory2, com.pIUnknown):
+    _methods_ = [
+        ('CreateGlyphRunAnalysis',
+         com.STDMETHOD()),
+        ('CreateCustomRenderingParams',
+         com.STDMETHOD()),
+        ('CreateFontFaceReference',
+         com.STDMETHOD()),
+        ('CreateFontFaceReference',
+         com.STDMETHOD()),
+        ('GetSystemFontSet',
+         com.STDMETHOD()),
+        ('CreateFontSetBuilder',
+         com.STDMETHOD(POINTER(IDWriteFontSetBuilder))),
+        ('CreateFontCollectionFromFontSet',
+         com.STDMETHOD(IDWriteFontSet, POINTER(IDWriteFontCollection1))),
+        ('GetSystemFontCollection3',
+         com.STDMETHOD()),
+        ('GetFontDownloadQueue',
+         com.STDMETHOD()),
+        #('GetSystemFontSet',
+        # com.STDMETHOD()),
+    ]
+
+
+class IDWriteColorGlyphRunEnumerator1(com.pIUnknown):
+    _methods_ = [
+        ('MoveNext',
+         com.STDMETHOD()),
+        ('GetCurrentRun',
+         com.STDMETHOD()),
+    ]
+
+class IDWriteFactory4(IDWriteFactory3, com.pIUnknown):
+    _methods_ = [
+        ('TranslateColorGlyphRun4',  # Renamed to prevent clash from previous factories.
+         com.STDMETHOD(D2D_POINT_2F, DWRITE_GLYPH_RUN, c_void_p, DWRITE_GLYPH_IMAGE_FORMATS, DWRITE_MEASURING_MODE, c_void_p, UINT32, POINTER(IDWriteColorGlyphRunEnumerator1))),
+        ('ComputeGlyphOrigins_',
+         com.STDMETHOD()),
+        ('ComputeGlyphOrigins',
+         com.STDMETHOD()),
+    ]
+
+
+class IDWriteInMemoryFontFileLoader(com.pIUnknown):
+    _methods_ = [
+        ('CreateStreamFromKey',
+         com.STDMETHOD()),
+        ('CreateInMemoryFontFileReference',
+         com.STDMETHOD(IDWriteFactory, c_void_p, UINT, c_void_p, POINTER(IDWriteFontFile))),
+        ('GetFileCount',
+         com.STDMETHOD()),
+    ]
+
+
+IID_IDWriteFactory5 = com.GUID(0x958DB99A, 0xBE2A, 0x4F09, 0xAF, 0x7D, 0x65, 0x18, 0x98, 0x03, 0xD1, 0xD3)
+
+
+class IDWriteFactory5(IDWriteFactory4, IDWriteFactory3, IDWriteFactory2, IDWriteFactory1, IDWriteFactory,
+                      com.pIUnknown):
+    _methods_ = [
+        ('CreateFontSetBuilder1',
+         com.STDMETHOD(POINTER(IDWriteFontSetBuilder1))),
+        ('CreateInMemoryFontFileLoader',
+         com.STDMETHOD(POINTER(IDWriteInMemoryFontFileLoader))),
+        ('CreateHttpFontFileLoader',
+         com.STDMETHOD()),
+        ('AnalyzeContainerType',
+         com.STDMETHOD())
+    ]
+
+
+
+DWriteCreateFactory = dwrite_lib.DWriteCreateFactory
+DWriteCreateFactory.restype = HRESULT
+DWriteCreateFactory.argtypes = [DWRITE_FACTORY_TYPE, com.REFIID, POINTER(com.pIUnknown)]
+
+
+class ID2D1Resource(com.pIUnknown):
+    _methods_ = [
+        ('GetFactory',
+         com.STDMETHOD()),
+    ]
+
+
+class ID2D1Brush(ID2D1Resource, com.pIUnknown):
+    _methods_ = [
+        ('SetOpacity',
+         com.STDMETHOD()),
+        ('SetTransform',
+         com.STDMETHOD()),
+        ('GetOpacity',
+         com.STDMETHOD()),
+        ('GetTransform',
+         com.STDMETHOD()),
+    ]
+
+
+class ID2D1SolidColorBrush(ID2D1Brush, ID2D1Resource, com.pIUnknown):
+    _methods_ = [
+        ('SetColor',
+         com.STDMETHOD()),
+        ('GetColor',
+         com.STDMETHOD()),
+    ]
+
+
+D2D1_TEXT_ANTIALIAS_MODE = UINT
+D2D1_TEXT_ANTIALIAS_MODE_DEFAULT = 0
+D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE = 1
+D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE = 2
+D2D1_TEXT_ANTIALIAS_MODE_ALIASED = 3
+
+D2D1_RENDER_TARGET_TYPE = UINT
+D2D1_RENDER_TARGET_TYPE_DEFAULT = 0
+D2D1_RENDER_TARGET_TYPE_SOFTWARE = 1
+D2D1_RENDER_TARGET_TYPE_HARDWARE = 2
+
+D2D1_FEATURE_LEVEL = UINT
+D2D1_FEATURE_LEVEL_DEFAULT = 0
+
+D2D1_RENDER_TARGET_USAGE = UINT
+D2D1_RENDER_TARGET_USAGE_NONE = 0
+D2D1_RENDER_TARGET_USAGE_FORCE_BITMAP_REMOTING = 1
+D2D1_RENDER_TARGET_USAGE_GDI_COMPATIBLE = 2
+
+DXGI_FORMAT = UINT
+DXGI_FORMAT_UNKNOWN = 0
+
+D2D1_ALPHA_MODE = UINT
+D2D1_ALPHA_MODE_UNKNOWN = 0
+D2D1_ALPHA_MODE_PREMULTIPLIED = 1
+D2D1_ALPHA_MODE_STRAIGHT = 2
+D2D1_ALPHA_MODE_IGNORE = 3
+
+D2D1_DRAW_TEXT_OPTIONS = UINT
+D2D1_DRAW_TEXT_OPTIONS_NO_SNAP = 0x00000001
+D2D1_DRAW_TEXT_OPTIONS_CLIP = 0x00000002
+D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT = 0x00000004
+D2D1_DRAW_TEXT_OPTIONS_DISABLE_COLOR_BITMAP_SNAPPING = 0x00000008
+D2D1_DRAW_TEXT_OPTIONS_NONE = 0x00000000
+D2D1_DRAW_TEXT_OPTIONS_FORCE_DWORD = 0xffffffff
+
+
+class D2D1_PIXEL_FORMAT(Structure):
+    _fields_ = (
+        ('format', DXGI_FORMAT),
+        ('alphaMode', D2D1_ALPHA_MODE),
+    )
+
+
+class D2D1_RENDER_TARGET_PROPERTIES(Structure):
+    _fields_ = (
+        ('type', D2D1_RENDER_TARGET_TYPE),
+        ('pixelFormat', D2D1_PIXEL_FORMAT),
+        ('dpiX', FLOAT),
+        ('dpiY', FLOAT),
+        ('usage', D2D1_RENDER_TARGET_USAGE),
+        ('minLevel', D2D1_FEATURE_LEVEL),
+    )
+
+
+DXGI_FORMAT_B8G8R8A8_UNORM = 87
+
+pixel_format = D2D1_PIXEL_FORMAT()
+pixel_format.format = DXGI_FORMAT_UNKNOWN
+pixel_format.alphaMode = D2D1_ALPHA_MODE_UNKNOWN
+
+default_target_properties = D2D1_RENDER_TARGET_PROPERTIES()
+default_target_properties.type = D2D1_RENDER_TARGET_TYPE_DEFAULT
+default_target_properties.pixelFormat = pixel_format
+default_target_properties.dpiX = 0.0
+default_target_properties.dpiY = 0.0
+default_target_properties.usage = D2D1_RENDER_TARGET_USAGE_NONE
+default_target_properties.minLevel = D2D1_FEATURE_LEVEL_DEFAULT
+
+
+class ID2D1RenderTarget(ID2D1Resource, com.pIUnknown):
+    _methods_ = [
+        ('CreateBitmap',
+         com.STDMETHOD()),
+        ('CreateBitmapFromWicBitmap',
+         com.STDMETHOD()),
+        ('CreateSharedBitmap',
+         com.STDMETHOD()),
+        ('CreateBitmapBrush',
+         com.STDMETHOD()),
+        ('CreateSolidColorBrush',
+         com.STDMETHOD(POINTER(D2D1_COLOR_F), c_void_p, POINTER(ID2D1SolidColorBrush))),
+        ('CreateGradientStopCollection',
+         com.STDMETHOD()),
+        ('CreateLinearGradientBrush',
+         com.STDMETHOD()),
+        ('CreateRadialGradientBrush',
+         com.STDMETHOD()),
+        ('CreateCompatibleRenderTarget',
+         com.STDMETHOD()),
+        ('CreateLayer',
+         com.STDMETHOD()),
+        ('CreateMesh',
+         com.STDMETHOD()),
+        ('DrawLine',
+         com.STDMETHOD()),
+        ('DrawRectangle',
+         com.STDMETHOD()),
+        ('FillRectangle',
+         com.STDMETHOD()),
+        ('DrawRoundedRectangle',
+         com.STDMETHOD()),
+        ('FillRoundedRectangle',
+         com.STDMETHOD()),
+        ('DrawEllipse',
+         com.STDMETHOD()),
+        ('FillEllipse',
+         com.STDMETHOD()),
+        ('DrawGeometry',
+         com.STDMETHOD()),
+        ('FillGeometry',
+         com.STDMETHOD()),
+        ('FillMesh',
+         com.STDMETHOD()),
+        ('FillOpacityMask',
+         com.STDMETHOD()),
+        ('DrawBitmap',
+         com.STDMETHOD()),
+        ('DrawText',
+         com.STDMETHOD(c_wchar_p, UINT, IDWriteTextFormat, POINTER(D2D1_RECT_F), ID2D1Brush, D2D1_DRAW_TEXT_OPTIONS,
+                       DWRITE_MEASURING_MODE)),
+        ('DrawTextLayout',
+         com.METHOD(c_void, D2D_POINT_2F, IDWriteTextLayout, ID2D1Brush, UINT32)),
+        ('DrawGlyphRun',
+         com.METHOD(c_void, D2D_POINT_2F, POINTER(DWRITE_GLYPH_RUN), ID2D1Brush, UINT32)),
+        ('SetTransform',
+         com.METHOD(c_void)),
+        ('GetTransform',
+         com.STDMETHOD()),
+        ('SetAntialiasMode',
+         com.STDMETHOD()),
+        ('GetAntialiasMode',
+         com.STDMETHOD()),
+        ('SetTextAntialiasMode',
+         com.METHOD(c_void, D2D1_TEXT_ANTIALIAS_MODE)),
+        ('GetTextAntialiasMode',
+         com.STDMETHOD()),
+        ('SetTextRenderingParams',
+         com.STDMETHOD()),
+        ('GetTextRenderingParams',
+         com.STDMETHOD()),
+        ('SetTags',
+         com.STDMETHOD()),
+        ('GetTags',
+         com.STDMETHOD()),
+        ('PushLayer',
+         com.STDMETHOD()),
+        ('PopLayer',
+         com.STDMETHOD()),
+        ('Flush',
+         com.STDMETHOD()),
+        ('SaveDrawingState',
+         com.STDMETHOD()),
+        ('RestoreDrawingState',
+         com.STDMETHOD()),
+        ('PushAxisAlignedClip',
+         com.STDMETHOD()),
+        ('PopAxisAlignedClip',
+         com.STDMETHOD()),
+        ('Clear',
+         com.METHOD(c_void, POINTER(D2D1_COLOR_F))),
+        ('BeginDraw',
+         com.METHOD(c_void)),
+        ('EndDraw',
+         com.STDMETHOD(c_void_p, c_void_p)),
+        ('GetPixelFormat',
+         com.STDMETHOD()),
+        ('SetDpi',
+         com.STDMETHOD()),
+        ('GetDpi',
+         com.STDMETHOD()),
+        ('GetSize',
+         com.STDMETHOD()),
+        ('GetPixelSize',
+         com.STDMETHOD()),
+        ('GetMaximumBitmapSize',
+         com.STDMETHOD()),
+        ('IsSupported',
+         com.STDMETHOD()),
+    ]
+
+
+IID_ID2D1Factory = com.GUID(0x06152247, 0x6f50, 0x465a, 0x92, 0x45, 0x11, 0x8b, 0xfd, 0x3b, 0x60, 0x07)
+
+
+class ID2D1Factory(com.pIUnknown):
+    _methods_ = [
+        ('ReloadSystemMetrics',
+         com.STDMETHOD()),
+        ('GetDesktopDpi',
+         com.STDMETHOD()),
+        ('CreateRectangleGeometry',
+         com.STDMETHOD()),
+        ('CreateRoundedRectangleGeometry',
+         com.STDMETHOD()),
+        ('CreateEllipseGeometry',
+         com.STDMETHOD()),
+        ('CreateGeometryGroup',
+         com.STDMETHOD()),
+        ('CreateTransformedGeometry',
+         com.STDMETHOD()),
+        ('CreatePathGeometry',
+         com.STDMETHOD()),
+        ('CreateStrokeStyle',
+         com.STDMETHOD()),
+        ('CreateDrawingStateBlock',
+         com.STDMETHOD()),
+        ('CreateWicBitmapRenderTarget',
+         com.STDMETHOD(IWICBitmap, POINTER(D2D1_RENDER_TARGET_PROPERTIES), POINTER(ID2D1RenderTarget))),
+        ('CreateHwndRenderTarget',
+         com.STDMETHOD()),
+        ('CreateDxgiSurfaceRenderTarget',
+         com.STDMETHOD()),
+        ('CreateDCRenderTarget',
+         com.STDMETHOD()),
+    ]
+
+
+d2d_lib = ctypes.windll.d2d1
+
+D2D1_FACTORY_TYPE = UINT
+D2D1_FACTORY_TYPE_SINGLE_THREADED = 0
+D2D1_FACTORY_TYPE_MULTI_THREADED = 1
+
+D2D1CreateFactory = d2d_lib.D2D1CreateFactory
+D2D1CreateFactory.restype = HRESULT
+D2D1CreateFactory.argtypes = [D2D1_FACTORY_TYPE, com.REFIID, c_void_p, c_void_p]
+
+# We need a WIC factory to make this work. Make sure one is in the initialized decoders.
+wic_decoder = None
+for decoder in pyglet.image.codecs.get_decoders():
+    if isinstance(decoder, WICDecoder):
+        wic_decoder = decoder
+
+if not wic_decoder:
+    raise Exception("Cannot use DirectWrite without a WIC Decoder")
+
+
+class DirectWriteGlyphRenderer(base.GlyphRenderer):
+    antialias_mode = D2D1_TEXT_ANTIALIAS_MODE_DEFAULT
+    draw_options = D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT
+
+    def __init__(self, font):
+        self._render_target = None
+        self._bitmap = None
+        self._brush = None
+        self._bitmap_dimensions = (0, 0)
+        super(DirectWriteGlyphRenderer, self).__init__(font)
+        self.font = font
+
+        self._analyzer = IDWriteTextAnalyzer()
+        self.font._write_factory.CreateTextAnalyzer(byref(self._analyzer))
+
+        self._text_analysis = TextAnalysis()
+
+    def render_to_image(self, text, width, height):
+        """This process takes Pyglet out of the equation and uses only DirectWrite to shape and render text.
+        This may allows more accurate fonts (bidi, rtl, etc) in very special circumstances."""
+        text_buffer = create_unicode_buffer(text)
+
+        text_layout = IDWriteTextLayout()
+        self.font._write_factory.CreateTextLayout(
+            text_buffer,
+            len(text_buffer),
+            self.font._text_format,
+            width,  # Doesn't affect bitmap size.
+            height,
+            byref(text_layout)
+        )
+
+        layout_metrics = DWRITE_TEXT_METRICS()
+        text_layout.GetMetrics(byref(layout_metrics))
+
+        width, height = int(math.ceil(layout_metrics.width)), int(math.ceil(layout_metrics.height))
+
+        bitmap = IWICBitmap()
+        wic_decoder._factory.CreateBitmap(
+            width,
+            height,
+            GUID_WICPixelFormat32bppPBGRA,
+            WICBitmapCacheOnDemand,
+            byref(bitmap)
+        )
+
+        rt = ID2D1RenderTarget()
+        d2d_factory.CreateWicBitmapRenderTarget(bitmap, default_target_properties, byref(rt))
+
+        # Font aliasing rendering quality.
+        rt.SetTextAntialiasMode(self.antialias_mode)
+
+        if not self._brush:
+            self._brush = ID2D1SolidColorBrush()
+
+        rt.CreateSolidColorBrush(white, None, byref(self._brush))
+
+        rt.BeginDraw()
+
+        rt.Clear(transparent)
+
+        rt.DrawTextLayout(no_offset,
+                          text_layout,
+                          self._brush,
+                          self.draw_options)
+
+        rt.EndDraw(None, None)
+
+        rt.Release()
+
+        image_data = wic_decoder.get_image(bitmap)
+
+        return image_data
+
+    def get_string_info(self, text, font_face):
+        """Converts a string of text into a list of indices and advances used for shaping."""
+        text_length = len(text.encode('utf-16-le')) // 2
+
+        # Unicode buffer splits each two byte chars into separate indices.
+        text_buffer = create_unicode_buffer(text, text_length)
+
+        # Analyze the text.
+        # noinspection PyTypeChecker
+        self._text_analysis.GenerateResults(self._analyzer, text_buffer, len(text_buffer))
+
+        # Formula for text buffer size from Microsoft.
+        max_glyph_size = int(3 * text_length / 2 + 16)
+
+        length = text_length
+        clusters = (UINT16 * length)()
+        text_props = (DWRITE_SHAPING_TEXT_PROPERTIES * length)()
+        indices = (UINT16 * max_glyph_size)()
+        glyph_props = (DWRITE_SHAPING_GLYPH_PROPERTIES * max_glyph_size)()
+        actual_count = UINT32()
+
+        self._analyzer.GetGlyphs(
+            text_buffer,
+            length,
+            font_face,
+            False,  # sideways
+            False,  # righttoleft
+            self._text_analysis._script,  # scriptAnalysis
+            None,  # localName
+            None,  # numberSub
+            None,  # typo features
+            None,  # feature range length
+            0,  # feature range
+            max_glyph_size,  # max glyph size
+            clusters,  # cluster map
+            text_props,  # text props
+            indices,  # glyph indices
+            glyph_props,  # glyph pops
+            byref(actual_count)  # glyph count
+        )
+
+        advances = (FLOAT * length)()
+        offsets = (DWRITE_GLYPH_OFFSET * length)()
+        self._analyzer.GetGlyphPlacements(
+            text_buffer,
+            clusters,
+            text_props,
+            text_length,
+            indices,
+            glyph_props,
+            actual_count,
+            font_face,
+            self.font._font_metrics.designUnitsPerEm,
+            False, False,
+            self._text_analysis._script,
+            self.font.locale,
+            None,
+            None,
+            0,
+            advances,
+            offsets
+        )
+
+        return text_buffer, actual_count.value, indices, advances, offsets, clusters
+
+    def get_glyph_metrics(self, font_face, indices, count):
+        """Returns a list of tuples with the following metrics per indice:
+            (glyph width, glyph height, lsb, advanceWidth)
+        """
+        glyph_metrics = (DWRITE_GLYPH_METRICS * count)()
+        font_face.GetDesignGlyphMetrics(indices, count, glyph_metrics, False)
+
+        metrics_out = []
+        i = 0
+        for metric in glyph_metrics:
+            glyph_width = (metric.advanceWidth - metric.leftSideBearing - metric.rightSideBearing)
+
+            # width must have a minimum of 1. For example, spaces are actually 0 width, still need glyph bitmap size.
+            if glyph_width == 0:
+                glyph_width = 1
+
+            glyph_height = (metric.advanceHeight - metric.topSideBearing - metric.bottomSideBearing)
+
+            lsb = metric.leftSideBearing
+
+            bsb = metric.bottomSideBearing
+
+            advance_width = metric.advanceWidth
+
+            metrics_out.append((glyph_width, glyph_height, lsb, advance_width, bsb))
+            i += 1
+
+        return metrics_out
+
+    def _get_single_glyph_run(self, font_face, size, indices, advances, offsets, sideways, bidi):
+        run = DWRITE_GLYPH_RUN(
+            font_face,
+            size,
+            1,
+            indices,
+            advances,
+            offsets,
+            sideways,
+            bidi
+        )
+        return run
+
+    def is_color_run(self, run):
+        """Will return True if the run contains a colored glyph."""
+        enumerator = IDWriteColorGlyphRunEnumerator1()
+        try:
+            color = self.font._write_factory.TranslateColorGlyphRun4(no_offset,
+                                                                     run,
+                                                                     None,
+                                                                     DWRITE_GLYPH_IMAGE_FORMATS_ALL,
+                                                                     DWRITE_MEASURING_MODE_NATURAL,
+                                                                     None,
+                                                                     0,
+                                                                     enumerator)
+
+            return True
+        except OSError:
+            # HRESULT returns -2003283956 (DWRITE_E_NOCOLOR) if no color run is detected.
+            pass
+
+        return False
+
+    def render_single_glyph(self, font_face, indice, advance, offset, metrics):
+        """Renders a single glyph using D2D DrawGlyphRun"""
+        glyph_width, glyph_height, lsb, font_advance, bsb = metrics  # We use a shaped advance instead of the fonts.
+
+        # Slicing an array turns it into a python object. Maybe a better way to keep it a ctypes value?
+        new_indice = (UINT16 * 1)(indice)
+        new_advance = (FLOAT * 1)(advance)
+
+        run = self._get_single_glyph_run(
+            font_face,
+            self.font._real_size,
+            new_indice,  # indice,
+            new_advance,  # advance,
+            pointer(offset),  # offset,
+            False,
+            False
+        )
+
+        # If it's colored, return to render it using layout.
+        if self.draw_options & D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT and self.is_color_run(run):
+            return None
+
+        render_width = int(math.ceil((glyph_width) * self.font.font_scale_ratio))
+        render_offset_x = int(math.floor(abs(lsb * self.font.font_scale_ratio)))
+        if lsb < 0:
+            # Negative LSB: we shift the layout rect to the right
+            # Otherwise we will cut the left part of the glyph
+            render_offset_x = -(render_offset_x)
+
+        # Create new bitmap.
+        # TODO: We can probably adjust bitmap/baseline to reduce the whitespace and save a lot of texture space.
+        # Note: Floating point precision makes this a giant headache, will need to be solved for this approach.
+        self._create_bitmap(render_width + 1,  # Add 1, sometimes AA can add an extra pixel or so.
+                            int(math.ceil(self.font.max_glyph_height)))
+
+        # Glyphs are drawn at the baseline, and with LSB, so we need to offset it based on top left position.
+        # Offsets are actually based on pixels somehow???
+        baseline_offset = D2D_POINT_2F(-render_offset_x - offset.advanceOffset,
+                                       self.font.ascent + offset.ascenderOffset)
+
+
+        self._render_target.BeginDraw()
+
+        self._render_target.Clear(transparent)
+
+        self._render_target.DrawGlyphRun(baseline_offset,
+                                         run,
+                                         self._brush,
+                                         DWRITE_MEASURING_MODE_NATURAL)
+
+        self._render_target.EndDraw(None, None)
+        image = wic_decoder.get_image(self._bitmap)
+
+        glyph = self.font.create_glyph(image)
+
+        glyph.set_bearings(self.font.descent, render_offset_x,
+                           advance * self.font.font_scale_ratio,
+                           offset.advanceOffset * self.font.font_scale_ratio,
+                           offset.ascenderOffset * self.font.font_scale_ratio)
+
+        return glyph
+
+    def render_using_layout(self, text):
+        """This will render text given the built in DirectWrite layout. This process allows us to take
+        advantage of color glyphs and fallback handling that is built into DirectWrite.
+        This can also handle shaping and many other features if you want to render directly to a texture."""
+        text_layout = self.font.create_text_layout(text)
+
+        layout_metrics = DWRITE_TEXT_METRICS()
+        text_layout.GetMetrics(byref(layout_metrics))
+
+        width = int(math.ceil(layout_metrics.width))
+        height = int(math.ceil(layout_metrics.height))
+
+        if width == 0 or height == 0:
+            return None
+
+        self._create_bitmap(width, height)
+
+        # This offsets the characters if needed.
+        point = D2D_POINT_2F(0, 0)
+
+        self._render_target.BeginDraw()
+
+        self._render_target.Clear(transparent)
+
+        self._render_target.DrawTextLayout(point,
+                                           text_layout,
+                                           self._brush,
+                                           self.draw_options)
+
+        self._render_target.EndDraw(None, None)
+
+        image = wic_decoder.get_image(self._bitmap)
+
+        glyph = self.font.create_glyph(image)
+        glyph.set_bearings(self.font.descent, 0, int(math.ceil(layout_metrics.width)))
+        return glyph
+
+    def _create_bitmap(self, width, height):
+        """Creates a bitmap using Direct2D and WIC."""
+        # Create a new bitmap, try to re-use the bitmap as much as we can to minimize creations.
+        if self._bitmap_dimensions[0] != width or self._bitmap_dimensions[1] != height:
+            # If dimensions aren't the same, release bitmap to create new ones.
+            if self._bitmap:
+                self._bitmap.Release()
+
+            self._bitmap = IWICBitmap()
+            wic_decoder._factory.CreateBitmap(width, height,
+                                              GUID_WICPixelFormat32bppPBGRA,
+                                              WICBitmapCacheOnDemand,
+                                              byref(self._bitmap))
+
+            self._render_target = ID2D1RenderTarget()
+            d2d_factory.CreateWicBitmapRenderTarget(self._bitmap, default_target_properties, byref(self._render_target))
+
+            # Font aliasing rendering quality.
+            self._render_target.SetTextAntialiasMode(self.antialias_mode)
+
+            if not self._brush:
+                self._brush = ID2D1SolidColorBrush()
+                self._render_target.CreateSolidColorBrush(white, None, byref(self._brush))
+
+
+class Win32DirectWriteFont(base.Font):
+    # To load fonts from files, we need to produce a custom collection.
+    _custom_collection = None
+
+    # Shared loader values
+    _write_factory = None  # Factory required to run any DirectWrite interfaces.
+    _font_loader = None
+
+    # Windows 10 loader values.
+    _font_builder = None
+    _font_set = None
+
+    # Legacy loader values
+    _font_collection_loader = None
+    _font_cache = []
+    _font_loader_key = None
+
+    _default_name = 'Segoe UI'  # Default font for Win7/10.
+
+    _glyph_renderer = None
+
+    glyph_renderer_class = DirectWriteGlyphRenderer
+    texture_internalformat = pyglet.gl.GL_RGBA
+
+    def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None, locale=None):
+        self._advance_cache = {}  # Stores glyph's by the indice and advance.
+
+        super(Win32DirectWriteFont, self).__init__()
+
+        if not name:
+            name = self._default_name
+
+        self._font_index, self._collection = self.get_collection(name)
+        assert self._collection is not None, "Font: {} not found in loaded or system font collection.".format(name)
+
+        self._name = name
+        self.bold = bold
+        self.size = size
+        self.italic = italic
+        self.stretch = stretch
+        self.dpi = dpi
+        self.locale = locale
+
+        if self.locale is None:
+            self.locale = ""
+            self.rtl = False  # Right to left should be handled by pyglet?
+            # TODO: Use system locale string?
+
+        if self.dpi is None:
+            self.dpi = 96
+
+        # From DPI to DIP (Device Independent Pixels) which is what the fonts rely on.
+        self._real_size = (self.size * self.dpi) // 72
+
+        if self.bold:
+            if type(self.bold) is str:
+                self._weight = name_to_weight[self.bold]
+            else:
+                self._weight = DWRITE_FONT_WEIGHT_BOLD
+        else:
+            self._weight = DWRITE_FONT_WEIGHT_NORMAL
+
+        if self.italic:
+            if type(self.italic) is str:
+                self._style = name_to_style[self.italic]
+            else:
+                self._style = DWRITE_FONT_STYLE_ITALIC
+        else:
+            self._style = DWRITE_FONT_STYLE_NORMAL
+
+        if self.stretch:
+            if type(self.stretch) is str:
+                self._stretch = name_to_stretch[self.stretch]
+            else:
+                self._stretch = DWRITE_FONT_STRETCH_EXPANDED
+        else:
+            self._stretch = DWRITE_FONT_STRETCH_NORMAL
+
+        # Create the text format this font will use permanently.
+        # Could technically be recreated, but will keep to be inline with other font objects.
+        self._text_format = IDWriteTextFormat()
+        self._write_factory.CreateTextFormat(
+            self._name,
+            self._collection,
+            self._weight,
+            self._style,
+            self._stretch,
+            self._real_size,
+            create_unicode_buffer(self.locale),
+            byref(self._text_format)
+        )
+
+        # All this work just to get a font face and it's metrics!
+        font_family = IDWriteFontFamily1()
+        self._collection.GetFontFamily(self._font_index, byref(font_family))
+
+        write_font = IDWriteFont()
+        font_family.GetFirstMatchingFont(
+            self._weight,
+            self._stretch,
+            self._style,
+            byref(write_font)
+        )
+
+        font_face = IDWriteFontFace()
+        write_font.CreateFontFace(byref(font_face))
+
+        self.font_face = IDWriteFontFace1()
+        font_face.QueryInterface(IID_IDWriteFontFace1, byref(self.font_face))
+
+        self._font_metrics = DWRITE_FONT_METRICS()
+        self.font_face.GetMetrics(byref(self._font_metrics))
+
+        self.font_scale_ratio = (self._real_size / self._font_metrics.designUnitsPerEm)
+
+        self.ascent = self._font_metrics.ascent * self.font_scale_ratio
+        self.descent = self._font_metrics.descent * self.font_scale_ratio
+
+        self.max_glyph_height = (self._font_metrics.ascent + self._font_metrics.descent) * self.font_scale_ratio
+        self.line_gap = self._font_metrics.lineGap * self.font_scale_ratio
+
+        self._fallback = None
+        if WINDOWS_8_1_OR_GREATER:
+            self._fallback = IDWriteFontFallback()
+            self._write_factory.GetSystemFontFallback(byref(self._fallback))
+        else:
+            assert _debug_font("Windows 8.1+ is required for font fallback. Colored glyphs cannot be omitted.")
+
+
+    @property
+    def name(self):
+        return self._name
+
+    def render_to_image(self, text, width=10000, height=80):
+        """This process takes Pyglet out of the equation and uses only DirectWrite to shape and render text.
+        This may allow more accurate fonts (bidi, rtl, etc) in very special circumstances at the cost of
+        additional texture space.
+
+        :Parameters:
+            `text` : str
+                String of text to render.
+
+        :rtype: `ImageData`
+        :return: An image of the text.
+        """
+        if not self._glyph_renderer:
+            self._glyph_renderer = self.glyph_renderer_class(self)
+
+        return self._glyph_renderer.render_to_image(text, width, height)
+
+    def copy_glyph(self, glyph, advance, offset):
+        """This takes the existing glyph texture and puts it into a new Glyph with a new advance.
+        Texture memory is shared between both glyphs."""
+        new_glyph = base.Glyph(glyph.x, glyph.y, glyph.z, glyph.width, glyph.height, glyph.owner)
+        new_glyph.set_bearings(
+            glyph.baseline,
+            glyph.lsb,
+            advance * self.font_scale_ratio,
+            offset.advanceOffset * self.font_scale_ratio,
+            offset.ascenderOffset * self.font_scale_ratio
+        )
+        return new_glyph
+
+    def _render_layout_glyph(self, text_buffer, i, clusters, check_color=True):
+        formatted_clusters = clusters[:]
+
+        # Some glyphs can be more than 1 char. We use the clusters to determine how many of an index exist.
+        text_length = formatted_clusters.count(i)
+
+        # Amount of glyphs don't always match 1:1 with text as some can be substituted or omitted. Get
+        # actual text buffer index.
+        text_index = formatted_clusters.index(i)
+
+        # Get actual text based on the index and length.
+        actual_text = text_buffer[text_index:text_index + text_length]
+
+        # Since we can't store as indice 0 without overriding, we have to store as text
+        if actual_text not in self.glyphs:
+            glyph = self._glyph_renderer.render_using_layout(text_buffer[text_index:text_index + text_length])
+            if glyph:
+                if check_color and self._glyph_renderer.draw_options & D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT:
+                    fb_ff = self._get_fallback_font_face(text_index, text_length)
+                    if fb_ff:
+                        glyph.colored = self.is_fallback_str_colored(fb_ff, actual_text)
+
+            self.glyphs[actual_text] = glyph
+
+        return self.glyphs[actual_text]
+
+    def is_fallback_str_colored(self, font_face, text):
+        indice = UINT16()
+        code_points = (UINT32 * len(text))(*[ord(c) for c in text])
+
+        font_face.GetGlyphIndices(code_points, len(text), byref(indice))
+
+        new_indice = (UINT16 * 1)(indice)
+        new_advance = (FLOAT * 1)(100)  # dummy
+        offset = (DWRITE_GLYPH_OFFSET * 1)()
+
+        run = self._glyph_renderer._get_single_glyph_run(
+            font_face,
+            self._real_size,
+            new_indice,  # indice,
+            new_advance,  # advance,
+            offset,  # offset,
+            False,
+            False
+        )
+
+        return self._glyph_renderer.is_color_run(run)
+
+    def _get_fallback_font_face(self, text_index, text_length):
+        if WINDOWS_8_1_OR_GREATER:
+            out_length = UINT32()
+            fb_font = IDWriteFont()
+            scale = FLOAT()
+
+            self._fallback.MapCharacters(
+                self._glyph_renderer._text_analysis,
+                text_index,
+                text_length,
+                None,
+                None,
+                self._weight,
+                self._style,
+                self._stretch,
+                byref(out_length),
+                byref(fb_font),
+                byref(scale)
+            )
+
+            if fb_font:
+                fb_font_face = IDWriteFontFace()
+                fb_font.CreateFontFace(byref(fb_font_face))
+
+                return fb_font_face
+
+        return None
+
+    def get_glyphs_no_shape(self, text):
+        """This differs in that it does not attempt to shape the text at all. May be useful in cases where your font
+        has no special shaping requirements, spacing is the same, or some other reason where faster performance is
+        wanted and you can get away with this."""
+        if not self._glyph_renderer:
+            self._glyph_renderer = self.glyph_renderer_class(self)
+
+        glyphs = []
+        for c in text:
+            if c == '\t':
+                c = ' '
+
+            if c not in self.glyphs:
+                self.glyphs[c] = self._glyph_renderer.render_using_layout(c)
+
+            if self.glyphs[c]:
+                glyphs.append(self.glyphs[c])
+
+        return glyphs
+
+    def get_glyphs(self, text):
+        if not self._glyph_renderer:
+            self._glyph_renderer = self.glyph_renderer_class(self)
+
+        text_buffer, actual_count, indices, advances, offsets, clusters = self._glyph_renderer.get_string_info(text, self.font_face)
+
+        metrics = self._glyph_renderer.get_glyph_metrics(self.font_face, indices, actual_count)
+
+        glyphs = []
+        for i in range(actual_count):
+            indice = indices[i]
+            if indice == 0:
+                # If an indice is 0, it will return no glyph. In this case we attempt to render leveraging
+                # the built in text layout from MS. Which depending on version can use fallback fonts and other tricks
+                # to possibly get something of use.
+                glyph = self._render_layout_glyph(text_buffer, i, clusters)
+                if glyph:
+                    glyphs.append(glyph)
+            else:
+                # Glyphs can vary depending on shaping. We will cache it by indice, advance, and offset.
+                # Possible to just cache without offset and set them each time. This may be faster?
+                if indice in self.glyphs:
+                    advance_key = (indice, advances[i], offsets[i].advanceOffset, offsets[i].ascenderOffset)
+                    if advance_key in self._advance_cache:
+                        glyph = self._advance_cache[advance_key]
+                    else:
+                        glyph = self.copy_glyph(self.glyphs[indice], advances[i], offsets[i])
+                        self._advance_cache[advance_key] = glyph
+                else:
+                    glyph = self._glyph_renderer.render_single_glyph(self.font_face, indice, advances[i], offsets[i],
+                                                                     metrics[i])
+                    if glyph is None:  # Will only return None if a color glyph is found. Use DW to render it directly.
+                        glyph = self._render_layout_glyph(text_buffer, i, clusters, check_color=False)
+                        glyph.colored = True
+
+                    self.glyphs[indice] = glyph
+                    self._advance_cache[(indice, advances[i], offsets[i].advanceOffset, offsets[i].ascenderOffset)] = glyph
+
+                glyphs.append(glyph)
+
+        return glyphs
+
+    def create_text_layout(self, text):
+        text_buffer = create_unicode_buffer(text)
+
+        text_layout = IDWriteTextLayout()
+        hr = self._write_factory.CreateTextLayout(text_buffer,
+                                                  len(text_buffer),
+                                                  self._text_format,
+                                                  10000,  # Doesn't affect bitmap size.
+                                                  80,
+                                                  byref(text_layout)
+                                                  )
+
+        return text_layout
+
+    @classmethod
+    def _initialize_direct_write(cls):
+        """ All direct write fonts needs factory access as well as the loaders."""
+        if WINDOWS_10_CREATORS_UPDATE_OR_GREATER:
+            cls._write_factory = IDWriteFactory5()
+            DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, IID_IDWriteFactory5, byref(cls._write_factory))
+        else:
+            # Windows 7 and 8 we need to create our own font loader, collection, enumerator, file streamer... Sigh.
+            cls._write_factory = IDWriteFactory()
+            DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, IID_IDWriteFactory, byref(cls._write_factory))
+
+    @classmethod
+    def _initialize_custom_loaders(cls):
+        """Initialize the loaders needed to load custom fonts."""
+        if WINDOWS_10_CREATORS_UPDATE_OR_GREATER:
+            # Windows 10 finally has a built in loader that can take data and make a font out of it w/ COMs.
+            cls._font_loader = IDWriteInMemoryFontFileLoader()
+            cls._write_factory.CreateInMemoryFontFileLoader(byref(cls._font_loader))
+            cls._write_factory.RegisterFontFileLoader(cls._font_loader)
+
+            # Used for grouping fonts together.
+            cls._font_builder = IDWriteFontSetBuilder1()
+            cls._write_factory.CreateFontSetBuilder1(byref(cls._font_builder))
+        else:
+            cls._font_loader = LegacyFontFileLoader()
+
+            # Note: RegisterFontLoader takes a pointer. However, for legacy we implement our own callback interface.
+            # Therefore we need to pass to the actual pointer directly.
+            cls._write_factory.RegisterFontFileLoader(cls._font_loader.pointers[IDWriteFontFileLoader])
+
+            cls._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader)
+            cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_loader)
+
+            cls._font_loader_key = cast(create_unicode_buffer("legacy_font_loader"), c_void_p)
+
+    @classmethod
+    def add_font_data(cls, data):
+        if not cls._write_factory:
+            cls._initialize_direct_write()
+
+        if not cls._font_loader:
+            cls._initialize_custom_loaders()
+
+        if WINDOWS_10_CREATORS_UPDATE_OR_GREATER:
+            font_file = IDWriteFontFile()
+            hr = cls._font_loader.CreateInMemoryFontFileReference(cls._write_factory,
+                                                                  data,
+                                                                  len(data),
+                                                                  None,
+                                                                  byref(font_file))
+
+            hr = cls._font_builder.AddFontFile(font_file)
+            if hr != 0:
+                raise Exception("This font file data is not not a font or unsupported.")
+
+            # We have to rebuild collection everytime we add a font.
+            # No way to add fonts to the collection once the FontSet and Collection are created.
+            # Release old one and renew.
+            if cls._custom_collection:
+                cls._font_set.Release()
+                cls._custom_collection.Release()
+
+            cls._font_set = IDWriteFontSet()
+            cls._font_builder.CreateFontSet(byref(cls._font_set))
+
+            cls._custom_collection = IDWriteFontCollection1()
+            cls._write_factory.CreateFontCollectionFromFontSet(cls._font_set, byref(cls._custom_collection))
+        else:
+            cls._font_cache.append(data)
+
+            # If a collection exists, we need to completely remake the collection, delete everything and start over.
+            if cls._custom_collection:
+                cls._custom_collection = None
+
+                cls._write_factory.UnregisterFontCollectionLoader(cls._font_collection_loader)
+                cls._write_factory.UnregisterFontFileLoader(cls._font_loader)
+
+                cls._font_loader = LegacyFontFileLoader()
+                cls._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader)
+
+                cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_loader)
+                cls._write_factory.RegisterFontFileLoader(cls._font_loader.pointers[IDWriteFontFileLoader])
+
+            cls._font_collection_loader.AddFontData(cls._font_cache)
+
+            cls._custom_collection = IDWriteFontCollection()
+
+            cls._write_factory.CreateCustomFontCollection(cls._font_collection_loader,
+                                                          cls._font_loader_key,
+                                                          sizeof(cls._font_loader_key),
+                                                          byref(cls._custom_collection))
+
+    @classmethod
+    def get_collection(cls, font_name):
+        """Returns which collection this font belongs to (system or custom collection), as well as it's index in the
+        collection."""
+        if not cls._write_factory:
+            cls._initialize_direct_write()
+
+        """Returns a collection the font_name belongs to."""
+        font_index = UINT()
+        font_exists = BOOL()
+
+        # Check custom loaded font collections.
+        if cls._custom_collection:
+            cls._custom_collection.FindFamilyName(create_unicode_buffer(font_name),
+                                                  byref(font_index),
+                                                  byref(font_exists))
+
+            if font_exists.value:
+                return font_index.value, cls._custom_collection
+
+        # Check if font is in the system collection.
+        # Do not cache these values permanently as system font collection can be updated during runtime.
+        if not font_exists.value:
+            sys_collection = IDWriteFontCollection()
+            cls._write_factory.GetSystemFontCollection(byref(sys_collection), 1)
+            sys_collection.FindFamilyName(create_unicode_buffer(font_name),
+                                          byref(font_index),
+                                          byref(font_exists))
+
+            if font_exists.value:
+                return font_index.value, sys_collection
+
+        # Font does not exist in either custom or system.
+        return None, None
+
+    @classmethod
+    def have_font(cls, name):
+        if cls.get_collection(name)[0] is not None:
+            return True
+
+        return False
+
+    @classmethod
+    def get_font_face(cls, name):
+        # Check custom collection.
+        collection = None
+        font_index = UINT()
+        font_exists = BOOL()
+
+        # Check custom collection.
+        if cls._custom_collection:
+            cls._custom_collection.FindFamilyName(create_unicode_buffer(name),
+                                                  byref(font_index),
+                                                  byref(font_exists))
+
+            collection = cls._custom_collection
+
+        if font_exists.value == 0:
+            sys_collection = IDWriteFontCollection()
+            cls._write_factory.GetSystemFontCollection(byref(sys_collection), 1)
+            sys_collection.FindFamilyName(create_unicode_buffer(name),
+                                          byref(font_index),
+                                          byref(font_exists))
+
+            collection = sys_collection
+
+        if font_exists:
+            font_family = IDWriteFontFamily()
+            collection.GetFontFamily(font_index, byref(font_family))
+
+            write_font = IDWriteFont()
+            font_family.GetFirstMatchingFont(DWRITE_FONT_WEIGHT_NORMAL,
+                                             DWRITE_FONT_STRETCH_NORMAL,
+                                             DWRITE_FONT_STYLE_NORMAL,
+                                             byref(write_font))
+
+            font_face = IDWriteFontFace1()
+            write_font.CreateFontFace(byref(font_face))
+
+            return font_face
+
+        return None
+
+
+d2d_factory = ID2D1Factory()
+hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, IID_ID2D1Factory, None, byref(d2d_factory))
+
+WICBitmapCreateCacheOption = UINT
+WICBitmapNoCache = 0
+WICBitmapCacheOnDemand = 0x1
+WICBitmapCacheOnLoad = 0x2
+
+transparent = D2D1_COLOR_F(0.0, 0.0, 0.0, 0.0)
+white = D2D1_COLOR_F(1.0, 1.0, 1.0, 1.0)
+no_offset = D2D_POINT_2F(0, 0)
+
+# If we are not shaping, monkeypatch to no shape function.
+if pyglet.options["win32_disable_shaping"]:
+    Win32DirectWriteFont.get_glyphs = Win32DirectWriteFont.get_glyphs_no_shape
\ No newline at end of file
diff --git a/pyglet/font/fontconfig.py b/pyglet/font/fontconfig.py
index 0ff8f1d..e0c489f 100644
--- a/pyglet/font/fontconfig.py
+++ b/pyglet/font/fontconfig.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/font/freetype.py b/pyglet/font/freetype.py
index 3308b22..9983db9 100644
--- a/pyglet/font/freetype.py
+++ b/pyglet/font/freetype.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -34,6 +34,7 @@
 # ----------------------------------------------------------------------------
 
 import ctypes
+import warnings
 from collections import namedtuple
 
 from pyglet.util import asbytes, asstr
@@ -45,7 +46,7 @@ from pyglet.font.freetype_lib import *
 
 class FreeTypeGlyphRenderer(base.GlyphRenderer):
     def __init__(self, font):
-        super(FreeTypeGlyphRenderer, self).__init__(font)
+        super().__init__(font)
         self.font = font
 
         self._glyph_slot = None
@@ -134,8 +135,7 @@ class FreeTypeGlyphRenderer(base.GlyphRenderer):
         return self._create_glyph()
 
 
-FreeTypeFontMetrics = namedtuple('FreeTypeFontMetrics',
-                                 ['ascent', 'descent'])
+FreeTypeFontMetrics = namedtuple('FreeTypeFontMetrics', ['ascent', 'descent'])
 
 
 class MemoryFaceStore:
@@ -160,10 +160,15 @@ class FreeTypeFont(base.Font):
     # Map font (name, bold, italic) to FreeTypeMemoryFace
     _memory_faces = MemoryFaceStore()
 
-    def __init__(self, name, size, bold=False, italic=False, dpi=None):
-        super(FreeTypeFont, self).__init__()
+    def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None):
+        # assert type(bold) is bool, "Only a boolean value is supported for bold in the current font renderer."
+        # assert type(italic) is bool, "Only a boolean value is supported for bold in the current font renderer."
 
-        self.name = name
+        if stretch:
+            warnings.warn("The current font render does not support stretching.")
+
+        super().__init__()
+        self._name = name
         self.size = size
         self.bold = bold
         self.italic = italic
@@ -172,6 +177,10 @@ class FreeTypeFont(base.Font):
         self._load_font_face()
         self.metrics = self.face.get_font_metrics(self.size, self.dpi)
 
+    @property
+    def name(self):
+        return self.face.family_name
+
     @property
     def ascent(self):
         return self.metrics.ascent
@@ -186,14 +195,14 @@ class FreeTypeFont(base.Font):
         return self.face.get_glyph_slot(glyph_index)
 
     def _load_font_face(self):
-        self.face = self._memory_faces.get(self.name, self.bold, self.italic)
+        self.face = self._memory_faces.get(self._name, self.bold, self.italic)
         if self.face is None:
             self._load_font_face_from_system()
 
     def _load_font_face_from_system(self):
-        match = get_fontconfig().find_font(self.name, self.size, self.bold, self.italic)
+        match = get_fontconfig().find_font(self._name, self.size, self.bold, self.italic)
         if not match:
-            raise base.FontException('Could not match font "%s"' % self.name)
+            raise base.FontException('Could not match font "%s"' % self._name)
         self.face = FreeTypeFace.from_fontconfig(match)
 
     @classmethod
@@ -241,6 +250,10 @@ class FreeTypeFace:
                 raise base.FontException('No filename for "%s"' % match.name)
             return cls.from_file(match.file)
 
+    @property
+    def name(self):
+        return self._name
+
     @property
     def family_name(self):
         return asstr(self.ft_face.contents.family_name)
@@ -315,7 +328,7 @@ class FreeTypeFace:
                                    descent=-ascent // 4)  # arbitrary.
 
     def _get_best_name(self):
-        self.name = self.family_name
+        self._name = asstr(self.ft_face.contents.family_name)
         self._get_font_family_from_ttf
 
     def _get_font_family_from_ttf(self):
@@ -334,7 +347,7 @@ class FreeTypeFace:
                             name.encoding_id == TT_MS_ID_UNICODE_CS):
                         continue
                     # name.string is not 0 terminated! use name.string_len
-                    self.name = name.string.decode('utf-16be', 'ignore')
+                    self._name = name.string.decode('utf-16be', 'ignore')
                 except:
                     continue
 
@@ -342,7 +355,7 @@ class FreeTypeFace:
 class FreeTypeMemoryFace(FreeTypeFace):
     def __init__(self, data):
         self._copy_font_data(data)
-        super(FreeTypeMemoryFace, self).__init__(self._create_font_face())
+        super().__init__(self._create_font_face())
 
     def _copy_font_data(self, data):
         self.font_data = (FT_Byte * len(data))()
diff --git a/pyglet/font/freetype_lib.py b/pyglet/font/freetype_lib.py
index 602946c..589d695 100644
--- a/pyglet/font/freetype_lib.py
+++ b/pyglet/font/freetype_lib.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/font/quartz.py b/pyglet/font/quartz.py
index a82f234..06a81c3 100644
--- a/pyglet/font/quartz.py
+++ b/pyglet/font/quartz.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -36,6 +36,7 @@
 # TODO Tiger and later: need to set kWindowApplicationScaledAttribute for DPI independence?
 
 import math
+import warnings
 from ctypes import c_void_p, c_int32, byref, c_byte
 
 from pyglet.font import base
@@ -50,7 +51,7 @@ quartz = cocoapy.quartz
 
 class QuartzGlyphRenderer(base.GlyphRenderer):
     def __init__(self, font):
-        super(QuartzGlyphRenderer, self).__init__(font)
+        super().__init__(font)
         self.font = font
 
     def render(self, text):
@@ -194,13 +195,19 @@ class QuartzFont(base.Font):
         cf.CFRelease(attributes)
         return descriptor
 
-    def __init__(self, name, size, bold=False, italic=False, dpi=None):
-        super(QuartzFont, self).__init__()
+    def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None):
+        # assert type(bold) is bool, "Only a boolean value is supported for bold in the current font renderer."
+        # assert type(italic) is bool, "Only a boolean value is supported for bold in the current font renderer."
 
-        if not name: name = 'Helvetica'
+        if stretch:
+            warnings.warn("The current font render does not support stretching.")
+
+        super().__init__()
+
+        name = name or 'Helvetica'
 
         # I don't know what is the right thing to do here.
-        if dpi is None: dpi = 96
+        dpi = dpi or 96
         size = size * dpi / 72.0
 
         # Construct traits value.
@@ -224,9 +231,17 @@ class QuartzFont(base.Font):
             cf.CFRelease(descriptor)
             assert self.ctFont, "Couldn't load font: " + name
 
+        string = c_void_p(ct.CTFontCopyFamilyName(self.ctFont))
+        self._family_name = str(cocoapy.cfstring_to_string(string))
+        cf.CFRelease(string)
+
         self.ascent = int(math.ceil(ct.CTFontGetAscent(self.ctFont)))
         self.descent = -int(math.ceil(ct.CTFontGetDescent(self.ctFont)))
 
+    @property
+    def name(self):
+        return self._family_name
+
     def __del__(self):
         cf.CFRelease(self.ctFont)
 
diff --git a/pyglet/font/ttf.py b/pyglet/font/ttf.py
index dc80660..864680a 100644
--- a/pyglet/font/ttf.py
+++ b/pyglet/font/ttf.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/font/win32.py b/pyglet/font/win32.py
index 609de2d..2d9a8f7 100644
--- a/pyglet/font/win32.py
+++ b/pyglet/font/win32.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -36,6 +36,7 @@
 # TODO Windows Vista: need to call SetProcessDPIAware?  May affect GDI+ calls as well as font.
 
 import math
+import warnings
 
 from sys import byteorder
 import pyglet
@@ -222,11 +223,11 @@ class GDIGlyphRenderer(Win32GlyphRenderer):
 class Win32Font(base.Font):
     glyph_renderer_class = GDIGlyphRenderer
 
-    def __init__(self, name, size, bold=False, italic=False, dpi=None):
+    def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None):
         super(Win32Font, self).__init__()
 
         self.logfont = self.get_logfont(name, size, bold, italic, dpi)
-        self.hfont = gdi32.CreateFontIndirectA(byref(self.logfont))
+        self.hfont = gdi32.CreateFontIndirectW(byref(self.logfont))
 
         # Create a dummy DC for coordinate mapping
         dc = user32.GetDC(0)
@@ -249,7 +250,7 @@ class Win32Font(base.Font):
             dpi = 96
         logpixelsy = dpi
 
-        logfont = LOGFONT()
+        logfont = LOGFONTW()
         # Conversion of point size to device pixels
         logfont.lfHeight = int(-size * logpixelsy // 72)
         if bold:
@@ -257,7 +258,7 @@ class Win32Font(base.Font):
         else:
             logfont.lfWeight = FW_NORMAL
         logfont.lfItalic = italic
-        logfont.lfFaceName = asbytes(name)
+        logfont.lfFaceName = name
         logfont.lfQuality = ANTIALIASED_QUALITY
         user32.ReleaseDC(0, dc)
         return logfont
@@ -350,30 +351,29 @@ class GDIPlusGlyphRenderer(Win32GlyphRenderer):
         self._bitmap_height = height
 
     def render(self, text):
-        
         ch = ctypes.create_unicode_buffer(text)
         len_ch = len(text)
 
         # Layout rectangle; not clipped against so not terribly important.
         width = 10000
         height = self._bitmap_height
-        rect = Rectf(0, self._bitmap_height 
-                        - self.font.ascent + self.font.descent, 
+        rect = Rectf(0, self._bitmap_height
+                        - self.font.ascent + self.font.descent,
                      width, height)
 
         # Set up GenericTypographic with 1 character measure range
         generic = ctypes.c_void_p()
         gdiplus.GdipStringFormatGetGenericTypographic(ctypes.byref(generic))
-        format = ctypes.c_void_p()
-        gdiplus.GdipCloneStringFormat(generic, ctypes.byref(format))
+        fmt = ctypes.c_void_p()
+        gdiplus.GdipCloneStringFormat(generic, ctypes.byref(fmt))
         gdiplus.GdipDeleteStringFormat(generic)
 
-        # Measure advance
-        
+        # --- Measure advance
+
         # XXX HACK HACK HACK
         # Windows GDI+ is a filthy broken toy.  No way to measure the bounding
         # box of a string, or to obtain LSB.  What a joke.
-        # 
+        #
         # For historical note, GDI cannot be used because it cannot composite
         # into a bitmap with alpha.
         #
@@ -381,7 +381,7 @@ class GDIPlusGlyphRenderer(Win32GlyphRenderer):
         # supporting accurate text measurement with alpha composition in .NET
         # 2.0 (WinForms) via the TextRenderer class; this has no C interface
         # though, so we're entirely screwed.
-        # 
+        #
         # So anyway, we first try to get the width with GdipMeasureString.
         # Then if it's a TrueType font, we use GetCharABCWidthsW to get the
         # correct LSB. If it's a negative LSB, we move the layoutRect `rect`
@@ -390,61 +390,77 @@ class GDIPlusGlyphRenderer(Win32GlyphRenderer):
         # space and we don't pass the LSB info to the Glyph.set_bearings
 
         bbox = Rectf()
-        flags = (StringFormatFlagsMeasureTrailingSpaces | 
-                 StringFormatFlagsNoClip | 
+        flags = (StringFormatFlagsMeasureTrailingSpaces |
+                 StringFormatFlagsNoClip |
                  StringFormatFlagsNoFitBlackBox)
-        gdiplus.GdipSetStringFormatFlags(format, flags)
-        gdiplus.GdipMeasureString(self._graphics, 
-                                  ch, 
+        gdiplus.GdipSetStringFormatFlags(fmt, flags)
+        gdiplus.GdipMeasureString(self._graphics,
+                                  ch,
                                   len_ch,
-                                  self.font._gdipfont, 
-                                  ctypes.byref(rect), 
-                                  format,
-                                  ctypes.byref(bbox), 
-                                  None, 
+                                  self.font._gdipfont,
+                                  ctypes.byref(rect),
+                                  fmt,
+                                  ctypes.byref(bbox),
+                                  None,
                                   None)
-        lsb = 0
+
+        # We only care about the advance from this whole thing.
         advance = int(math.ceil(bbox.width))
-        width = advance
-        
-        # This hack bumps up the width if the font is italic;
-        # this compensates for some common fonts.  It's also a stupid 
-        # waste of texture memory.
-        if self.font.italic:
-            width += width // 2
-            # Do not enlarge more than the _rect width.
-            width = min(width, self._rect.Width) 
-        
+
         # GDI functions only work for a single character so we transform
         # grapheme \r\n into \r
         if text == '\r\n':
             text = '\r'
 
-        abc = ABC()
-        # Check if ttf font.         
-        if gdi32.GetCharABCWidthsW(self._dc, 
-            ord(text), ord(text), byref(abc)):
-            
-            lsb = abc.abcA
-            if lsb < 0:
-                # Negative LSB: we shift the layout rect to the right
-                # Otherwise we will cut the left part of the glyph
-                rect.x = -lsb
-                width -= lsb
         # XXX END HACK HACK HACK
 
+        abc = ABC()
+        width = 0
+        lsb = 0
+        ttf_font = True
+        # Use GDI to get code points for the text passed. This is almost always 1.
+        # For special unicode characters it may be comprised of 2+ codepoints. Get the width/lsb of each.
+        # Function only works on TTF fonts.
+        for codepoint in [ord(c) for c in text]:
+            if gdi32.GetCharABCWidthsW(self._dc, codepoint, codepoint, byref(abc)):
+                lsb += abc.abcA
+                width += abc.abcB
+
+                if lsb < 0:
+                    # Negative LSB: we shift the layout rect to the right
+                    # Otherwise we will cut the left part of the glyph
+                    rect.x = -lsb
+                    width -= lsb
+                else:
+                    width += lsb
+            else:
+                ttf_font = False
+                break
+
+        # Almost always a TTF font. Haven't seen a modern font that GetCharABCWidthsW fails on.
+        # For safety, just use the advance as the width.
+        if not ttf_font:
+            width = advance
+
+            # This hack bumps up the width if the font is italic;
+            # this compensates for some common fonts.  It's also a stupid
+            # waste of texture memory.
+            if self.font.italic:
+                width += width // 2
+                # Do not enlarge more than the _rect width.
+                width = min(width, self._rect.Width)
+
         # Draw character to bitmap
-        
         gdiplus.GdipGraphicsClear(self._graphics, 0x00000000)
         gdiplus.GdipDrawString(self._graphics, 
                                ch,
                                len_ch,
                                self.font._gdipfont, 
                                ctypes.byref(rect), 
-                               format,
+                               fmt,
                                self._brush)
         gdiplus.GdipFlush(self._graphics, 1)
-        gdiplus.GdipDeleteStringFormat(format)
+        gdiplus.GdipDeleteStringFormat(fmt)
 
         bitmap_data = BitmapData()
         gdiplus.GdipBitmapLockBits(self._bitmap, 
@@ -459,14 +475,13 @@ class GDIPlusGlyphRenderer(Win32GlyphRenderer):
         # Unlock data
         gdiplus.GdipBitmapUnlockBits(self._bitmap, byref(bitmap_data))
         
-        image = pyglet.image.ImageData(width, height, 
+        image = pyglet.image.ImageData(width, height,
             'BGRA', buffer, -bitmap_data.Stride)
 
         glyph = self.font.create_glyph(image)
         # Only pass negative LSB info
         lsb = min(lsb, 0)
         glyph.set_bearings(-self.font.descent, lsb, advance)
-
         return glyph
 
 FontStyleBold = 1
@@ -481,29 +496,35 @@ class GDIPlusFont(Win32Font):
 
     _default_name = 'Arial'
 
-    def __init__(self, name, size, bold=False, italic=False, dpi=None):
+    def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None):
         if not name:
             name = self._default_name
-        super(GDIPlusFont, self).__init__(name, size, bold, italic, dpi)
+
+        # assert type(bold) is bool, "Only a boolean value is supported for bold in the current font renderer."
+        # assert type(italic) is bool, "Only a boolean value is supported for bold in the current font renderer."
+
+        if stretch:
+            warnings.warn("The current font render does not support stretching.")
+
+        super().__init__(name, size, bold, italic, stretch, dpi)
+
+        self._name = name
 
         family = ctypes.c_void_p()
         name = ctypes.c_wchar_p(name)
 
         # Look in private collection first:
         if self._private_fonts:
-            gdiplus.GdipCreateFontFamilyFromName(name,
-                self._private_fonts, ctypes.byref(family)) 
+            gdiplus.GdipCreateFontFamilyFromName(name, self._private_fonts, ctypes.byref(family))
 
         # Then in system collection:
         if not family:
-            gdiplus.GdipCreateFontFamilyFromName(name,
-                None, ctypes.byref(family)) 
+            gdiplus.GdipCreateFontFamilyFromName(name, None, ctypes.byref(family))
 
         # Nothing found, use default font.
         if not family:
-            name = self._default_name
-            gdiplus.GdipCreateFontFamilyFromName(ctypes.c_wchar_p(name),
-                None, ctypes.byref(family)) 
+            self._name = self._default_name
+            gdiplus.GdipCreateFontFamilyFromName(ctypes.c_wchar_p(self._name), None, ctypes.byref(family))
 
         if dpi is None:
             unit = UnitPoint
@@ -519,13 +540,16 @@ class GDIPlusFont(Win32Font):
         if italic:
             style |= FontStyleItalic
         self._gdipfont = ctypes.c_void_p()
-        gdiplus.GdipCreateFont(family, ctypes.c_float(size),
-            style, unit, ctypes.byref(self._gdipfont))
+        gdiplus.GdipCreateFont(family, ctypes.c_float(size), style, unit, ctypes.byref(self._gdipfont))
         gdiplus.GdipDeleteFontFamily(family)
 
+    @property
+    def name(self):
+        return self._name
+
     def __del__(self):
         super(GDIPlusFont, self).__del__()
-        result = gdiplus.GdipDeleteFont(self._gdipfont)
+        gdiplus.GdipDeleteFont(self._gdipfont)
 
     @classmethod
     def add_font_data(cls, data):
diff --git a/pyglet/font/win32query.py b/pyglet/font/win32query.py
index 7a7e99c..d239a5f 100644
--- a/pyglet/font/win32query.py
+++ b/pyglet/font/win32query.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -153,6 +153,7 @@ but the same code using GDI+ rendered 16,600 glyphs per second.
 
 import ctypes
 from ctypes import wintypes
+from pyglet.libs.win32 import LOGFONT, LOGFONTW
 
 user32 = ctypes.windll.user32
 gdi32 = ctypes.windll.gdi32
@@ -209,62 +210,6 @@ FF_SCRIPT = 4  # handwritten
 FF_DECORATIVE = 5  # novelty
 
 
-class LOGFONT(ctypes.Structure):
-    # EnumFontFamiliesEx examines only 3 fields:
-    #  - lfCharSet
-    #  - lfFaceName  - empty string enumerates one font in each available
-    #                  typeface name, valid typeface name gets all fonts
-    #                  with that name
-    #  - lfPitchAndFamily - must be set to 0 [ ]
-    _fields_ = [
-        ('lfHeight', wintypes.LONG),
-        # value > 0  specifies the largest size of *char cell* to match
-        #            char cell = char height + internal leading
-        # value = 0  makes matched use default height for search
-        # value < 0  specifies the largest size of *char height* to match
-        ('lfWidth', wintypes.LONG),
-        # average width also in *logical units*, which are pixels in
-        # default _mapping mode_ (MM_TEXT) for device
-        ('lfEscapement', wintypes.LONG),
-        # string baseline rotation in tenths of degrees
-        ('lfOrientation', wintypes.LONG),
-        # character rotation in tenths of degrees
-        ('lfWeight', wintypes.LONG),
-        # 0 through 1000  400 is normal, 700 is bold, 0 is default
-        ('lfItalic', BYTE),
-        ('lfUnderline', BYTE),
-        ('lfStrikeOut', BYTE),
-        ('lfCharSet', BYTE),
-        # ANSI_CHARSET, BALTIC_CHARSET, ... - see *_CHARSET constants above
-        ('lfOutPrecision', BYTE),
-        # many constants how the output must match height, width, pitch etc.
-        # OUT_DEFAULT_PRECIS
-        # [ ] TODO
-        ('lfClipPrecision', BYTE),
-        # how to clip characters, no useful properties, leave default value
-        # CLIP_DEFAULT_PRECIS
-        ('lfQuality', BYTE),
-        # ANTIALIASED_QUALITY
-        # CLEARTYPE_QUALITY
-        # DEFAULT_QUALITY
-        # DRAFT_QUALITY
-        # NONANTIALIASED_QUALITY
-        # PROOF_QUALITY
-        ('lfPitchAndFamily', BYTE),
-        # DEFAULT_PITCH
-        # FIXED_PITCH      - authoritative for monospace
-        # VARIABLE_PITCH
-        #    stacked with any of
-        # FF_DECORATIVE   - novelty
-        # FF_DONTCARE     - default font
-        # FF_MODERN       - stroke width ('pen width') near constant
-        # FF_ROMAN        - proportional (variable char width) with serifs
-        # FF_SCRIPT       - handwritten
-        # FF_SWISS        - proportional without serifs
-        ('lfFaceName', TCHAR * 32)]
-    # typeface name of the font - null-terminated string
-
-
 class FONTSIGNATURE(ctypes.Structure):
     # supported code pages and Unicode subranges for the font
     # needed for NEWTEXTMETRICEX structure
@@ -302,6 +247,32 @@ class NEWTEXTMETRIC(ctypes.Structure):
         ('ntmCellHeight', wintypes.UINT),
         ('ntmAvgWidth', wintypes.UINT)]
 
+class NEWTEXTMETRICW(ctypes.Structure):
+    _fields_ = [
+        ('tmHeight', wintypes.LONG),
+        ('tmAscent', wintypes.LONG),
+        ('tmDescent', wintypes.LONG),
+        ('tmInternalLeading', wintypes.LONG),
+        ('tmExternalLeading', wintypes.LONG),
+        ('tmAveCharWidth', wintypes.LONG),
+        ('tmMaxCharWidth', wintypes.LONG),
+        ('tmWeight', wintypes.LONG),
+        ('tmOverhang', wintypes.LONG),
+        ('tmDigitizedAspectX', wintypes.LONG),
+        ('tmDigitizedAspectY', wintypes.LONG),
+        ('mFirstChar', wintypes.WCHAR),
+        ('mLastChar', wintypes.WCHAR),
+        ('mDefaultChar', wintypes.WCHAR),
+        ('mBreakChar', wintypes.WCHAR),
+        ('tmItalic', BYTE),
+        ('tmUnderlined', BYTE),
+        ('tmStruckOut', BYTE),
+        ('tmPitchAndFamily', BYTE),
+        ('tmCharSet', BYTE),
+        ('tmFlags', wintypes.DWORD),
+        ('ntmSizeEM', wintypes.UINT),
+        ('ntmCellHeight', wintypes.UINT),
+        ('ntmAvgWidth', wintypes.UINT)]
 
 class NEWTEXTMETRICEX(ctypes.Structure):
     # physical font attributes for True Type fonts
@@ -310,6 +281,10 @@ class NEWTEXTMETRICEX(ctypes.Structure):
         ('ntmTm', NEWTEXTMETRIC),
         ('ntmFontSig', FONTSIGNATURE)]
 
+class NEWTEXTMETRICEXW(ctypes.Structure):
+    _fields_ = [
+        ('ntmTm', NEWTEXTMETRICW),
+        ('ntmFontSig', FONTSIGNATURE)]
 
 # type for a function that is called by the system for
 # each font during execution of EnumFontFamiliesEx
@@ -324,6 +299,15 @@ FONTENUMPROC = ctypes.WINFUNCTYPE(
     wintypes.LPARAM
 )
 
+FONTENUMPROCW = ctypes.WINFUNCTYPE(
+    ctypes.c_int,  # return non-0 to continue enumeration, 0 to stop
+    ctypes.POINTER(LOGFONTW),
+    ctypes.POINTER(NEWTEXTMETRICEXW),
+    wintypes.DWORD,
+    wintypes.LPARAM
+)
+
+
 # When running 64 bit windows, some types are not 32 bit, so Python/ctypes guesses wrong
 gdi32.EnumFontFamiliesExA.argtypes = [
     wintypes.HDC,
@@ -333,6 +317,13 @@ gdi32.EnumFontFamiliesExA.argtypes = [
     wintypes.DWORD]
 
 
+gdi32.EnumFontFamiliesExW.argtypes = [
+    wintypes.HDC,
+    ctypes.POINTER(LOGFONTW),
+    FONTENUMPROCW,
+    wintypes.LPARAM,
+    wintypes.DWORD]
+
 def _enum_font_names(logfont, textmetricex, fonttype, param):
     """callback function to be executed during EnumFontFamiliesEx
        call for each font name. it stores names in global variable
@@ -340,7 +331,7 @@ def _enum_font_names(logfont, textmetricex, fonttype, param):
     global FONTDB
 
     lf = logfont.contents
-    name = lf.lfFaceName.decode('utf-8')
+    name = lf.lfFaceName
 
     # detect font type (vector|raster) and format (ttf)
     # [ ] use Windows constant TRUETYPE_FONTTYPE
@@ -406,7 +397,7 @@ def _enum_font_names(logfont, textmetricex, fonttype, param):
     return 1  # non-0 to continue enumeration
 
 
-enum_font_names = FONTENUMPROC(_enum_font_names)
+enum_font_names = FONTENUMPROCW(_enum_font_names)
 
 
 # --- /define
@@ -445,9 +436,9 @@ def query(charset=DEFAULT_CHARSET):
     #      - enumerate all available charsets for a single font
     #      - other params?
 
-    logfont = LOGFONT(0, 0, 0, 0, 0, 0, 0, 0, charset, 0, 0, 0, 0, b'\0')
+    logfont = LOGFONTW(0, 0, 0, 0, 0, 0, 0, 0, charset, 0, 0, 0, 0, '')
     FONTDB = []  # clear cached FONTDB for enum_font_names callback
-    res = gdi32.EnumFontFamiliesExA(
+    res = gdi32.EnumFontFamiliesExW(
         hdc,  # handle to device context
         ctypes.byref(logfont),
         enum_font_names,  # pointer to callback function
diff --git a/pyglet/gl/__init__.py b/pyglet/gl/__init__.py
index ae1e7e6..5e4c0a0 100644
--- a/pyglet/gl/__init__.py
+++ b/pyglet/gl/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -212,6 +212,9 @@ def _create_shadow_window():
 
 if _is_pyglet_doc_run:
     from .base import Config
+
+elif _pyglet.options['headless']:
+    from .headless import HeadlessConfig as Config
 elif compat_platform in ('win32', 'cygwin'):
     from .win32 import Win32Config as Config
 elif compat_platform.startswith('linux'):
diff --git a/pyglet/gl/agl.py b/pyglet/gl/agl.py
index 499aced..731a5b3 100644
--- a/pyglet/gl/agl.py
+++ b/pyglet/gl/agl.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/base.py b/pyglet/gl/base.py
index f5eec4e..e63102f 100755
--- a/pyglet/gl/base.py
+++ b/pyglet/gl/base.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -171,9 +171,8 @@ class Config:
         :rtype: `Context`
         :return: The new context.
         """
-        raise gl.ConfigException(
-            'This config cannot be used to create contexts.  '
-            'Use Config.match to created a CanvasConfig')
+        raise gl.ConfigException('This config cannot be used to create contexts.  '
+                                 'Use Config.match to created a CanvasConfig')
 
     def is_complete(self):
         """Determine if this config is complete and able to create a context.
@@ -192,8 +191,8 @@ class Config:
 
     def __repr__(self):
         import pprint
-        return '%s(%s)' % (self.__class__.__name__, 
-                           pprint.pformat(self.get_gl_attributes()))
+        return '%s(%s)' % (self.__class__.__name__, pprint.pformat(self.get_gl_attributes()))
+
 
 class CanvasConfig(Config):
     """OpenGL configuration for a particular canvas.
@@ -207,6 +206,7 @@ class CanvasConfig(Config):
             The canvas this config is valid on.
 
     """
+
     def __init__(self, canvas, base_config):
         self.canvas = canvas
 
@@ -232,15 +232,16 @@ class CanvasConfig(Config):
 
     def is_complete(self):
         return True
- 
+
 
 class ObjectSpace:
     def __init__(self):
-        # Textures and buffers scheduled for deletion the next time this
-        # object space is active.
+        # Textures and buffers scheduled for deletion
+        # the next time this object space is active.
         self._doomed_textures = []
         self._doomed_buffers = []
 
+
 class Context:
     """OpenGL context for drawing.
 
@@ -260,7 +261,7 @@ class Context:
     #: Context share behaviour indicating that objects are shared with
     #: the most recently created context (the default).
     CONTEXT_SHARE_EXISTING = 1
-    
+
     # Used for error checking, True if currently within a glBegin/End block.
     # Ignored if error checking is disabled.
     _gl_begin = False
@@ -287,14 +288,14 @@ class Context:
         ('_workaround_vbo',
          lambda info: (info.get_renderer().startswith('ATI Radeon X')
                        or info.get_renderer().startswith('RADEON XPRESS 200M')
-                       or info.get_renderer() == 
-                            'Intel 965/963 Graphics Media Accelerator')),
+                       or info.get_renderer() ==
+                       'Intel 965/963 Graphics Media Accelerator')),
 
         # Some ATI cards on OS X start drawing from a VBO before it's written
         # to.  In these cases pyglet needs to call glFinish() to flush the
         # pipeline after updating a buffer but before rendering.
         ('_workaround_vbo_finish',
-         lambda info: ('ATI' in info.get_renderer() and 
+         lambda info: ('ATI' in info.get_renderer() and
                        info.have_version(1, 5) and
                        compat_platform == 'darwin')),
     ]
@@ -308,7 +309,7 @@ class Context:
             self.object_space = context_share.object_space
         else:
             self.object_space = ObjectSpace()
-    
+
     def __repr__(self):
         return '%s()' % self.__class__.__name__
 
diff --git a/pyglet/gl/cocoa.py b/pyglet/gl/cocoa.py
index e68262e..e75dcdf 100644
--- a/pyglet/gl/cocoa.py
+++ b/pyglet/gl/cocoa.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/gl.py b/pyglet/gl/gl.py
index 360acea..ea81040 100644
--- a/pyglet/gl/gl.py
+++ b/pyglet/gl/gl.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/gl_info.py b/pyglet/gl/gl_info.py
index 12749ab..70f21d5 100644
--- a/pyglet/gl/gl_info.py
+++ b/pyglet/gl/gl_info.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/glext_arb.py b/pyglet/gl/glext_arb.py
index 6ff70c8..e3661bc 100644
--- a/pyglet/gl/glext_arb.py
+++ b/pyglet/gl/glext_arb.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/glext_nv.py b/pyglet/gl/glext_nv.py
index 323290c..bad7936 100644
--- a/pyglet/gl/glext_nv.py
+++ b/pyglet/gl/glext_nv.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/glu.py b/pyglet/gl/glu.py
index edb8c2a..f9c26d9 100644
--- a/pyglet/gl/glu.py
+++ b/pyglet/gl/glu.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/glu_info.py b/pyglet/gl/glu_info.py
index 8316719..a520268 100644
--- a/pyglet/gl/glu_info.py
+++ b/pyglet/gl/glu_info.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/glx.py b/pyglet/gl/glx.py
index c481356..180262b 100644
--- a/pyglet/gl/glx.py
+++ b/pyglet/gl/glx.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/glx_info.py b/pyglet/gl/glx_info.py
index b4524ec..57b8436 100644
--- a/pyglet/gl/glx_info.py
+++ b/pyglet/gl/glx_info.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/glxext_arb.py b/pyglet/gl/glxext_arb.py
index 5f0fdbb..aea60a0 100644
--- a/pyglet/gl/glxext_arb.py
+++ b/pyglet/gl/glxext_arb.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/glxext_mesa.py b/pyglet/gl/glxext_mesa.py
index ca3b618..045fec4 100644
--- a/pyglet/gl/glxext_mesa.py
+++ b/pyglet/gl/glxext_mesa.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/glxext_nv.py b/pyglet/gl/glxext_nv.py
index 5e1b621..19c5510 100644
--- a/pyglet/gl/glxext_nv.py
+++ b/pyglet/gl/glxext_nv.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/headless.py b/pyglet/gl/headless.py
new file mode 100644
index 0000000..3622df2
--- /dev/null
+++ b/pyglet/gl/headless.py
@@ -0,0 +1,184 @@
+# ----------------------------------------------------------------------------
+# pyglet
+# Copyright (c) 2006-2008 Alex Holkner
+# Copyright (c) 2008-2022 pyglet contributors
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+#  * Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+#  * Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in
+#    the documentation and/or other materials provided with the
+#    distribution.
+#  * Neither the name of pyglet nor the names of its
+#    contributors may be used to endorse or promote products
+#    derived from this software without specific prior written
+#    permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# ----------------------------------------------------------------------------
+
+import warnings
+from ctypes import *
+
+from .base import Config, CanvasConfig, Context
+from pyglet.canvas.headless import HeadlessCanvas
+from pyglet.libs.egl import egl
+from pyglet.libs.egl.egl import *
+from pyglet import gl
+
+
+_fake_gl_attributes = {
+    'double_buffer': 0,
+    'stereo': 0,
+    'aux_buffers': 0,
+    'accum_red_size': 0,
+    'accum_green_size': 0,
+    'accum_blue_size': 0,
+    'accum_alpha_size': 0
+}
+
+class HeadlessConfig(Config):
+    def match(self, canvas):
+        if not isinstance(canvas, HeadlessCanvas):
+            raise RuntimeError('Canvas must be an instance of HeadlessCanvas')
+
+        display_connection = canvas.display._display_connection
+
+        # Construct array of attributes
+        attrs = []
+        for name, value in self.get_gl_attributes():
+            if name == 'double_buffer':
+                continue
+            attr = HeadlessCanvasConfig.attribute_ids.get(name, None)
+            if attr and value is not None:
+                attrs.extend([attr, int(value)])
+        attrs.extend([EGL_SURFACE_TYPE, EGL_PBUFFER_BIT])
+        attrs.extend([EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT])
+        attrs.extend([EGL_NONE])
+        attrs_list = (egl.EGLint * len(attrs))(*attrs)
+
+        num_config = egl.EGLint()
+        egl.eglChooseConfig(display_connection, attrs_list, None, 0, byref(num_config))
+        configs = (egl.EGLConfig * num_config.value)()
+        egl.eglChooseConfig(display_connection, attrs_list, configs,
+                            num_config.value, byref(num_config))
+
+        result = [HeadlessCanvasConfig(canvas, c, self) for c in configs]
+        return result
+
+
+class HeadlessCanvasConfig(CanvasConfig):
+    attribute_ids = {
+        'buffer_size': egl.EGL_BUFFER_SIZE,
+        'level': egl.EGL_LEVEL,  # Not supported
+        'red_size': egl.EGL_RED_SIZE,
+        'green_size': egl.EGL_GREEN_SIZE,
+        'blue_size': egl.EGL_BLUE_SIZE,
+        'alpha_size': egl.EGL_ALPHA_SIZE,
+        'depth_size': egl.EGL_DEPTH_SIZE,
+        'stencil_size': egl.EGL_STENCIL_SIZE,
+        'sample_buffers': egl.EGL_SAMPLE_BUFFERS,
+        'samples': egl.EGL_SAMPLES,
+    }
+
+    def __init__(self, canvas, egl_config, config):
+        super(HeadlessCanvasConfig, self).__init__(canvas, config)
+        self._egl_config = egl_config
+        context_attribs = (EGL_CONTEXT_MAJOR_VERSION, config.major_version or 2,
+                           EGL_CONTEXT_MINOR_VERSION, config.minor_version or 0,
+                           EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE, config.forward_compatible or 0,
+                           EGL_CONTEXT_OPENGL_DEBUG, config.debug or 0,
+                           EGL_NONE)
+        self._context_attrib_array = (egl.EGLint * len(context_attribs))(*context_attribs)
+
+        for name, attr in self.attribute_ids.items():
+            value = egl.EGLint()
+            egl.eglGetConfigAttrib(canvas.display._display_connection, egl_config, attr, byref(value))
+            setattr(self, name, value.value)
+
+        for name, value in _fake_gl_attributes.items():
+            setattr(self, name, value)
+
+    def compatible(self, canvas):
+        # TODO check more
+        return isinstance(canvas, HeadlessCanvas)
+
+    def create_context(self, share):
+        return HeadlessContext(self, share)
+
+
+class HeadlessContext(Context):
+    def __init__(self, config, share):
+        super(HeadlessContext, self).__init__(config, share)
+
+        self.display_connection = config.canvas.display._display_connection
+
+        self.egl_context = self._create_egl_context(share)
+        if not self.egl_context:
+            raise gl.ContextException('Could not create GL context')
+
+    def _create_egl_context(self, share):
+        if share:
+            share_context = share.egl_context
+        else:
+            share_context = None
+
+        egl.eglBindAPI(egl.EGL_OPENGL_API)
+        return egl.eglCreateContext(self.config.canvas.display._display_connection,
+                                    self.config._egl_config, share_context,
+                                    self.config._context_attrib_array)
+
+    def attach(self, canvas):
+        if canvas is self.canvas:
+            return
+
+        super(HeadlessContext, self).attach(canvas)
+
+        self.egl_surface = canvas.egl_surface
+        self.set_current()
+
+    def set_current(self):
+        egl.eglMakeCurrent(
+            self.display_connection, self.egl_surface, self.egl_surface, self.egl_context)
+        super(HeadlessContext, self).set_current()
+
+    def detach(self):
+        if not self.canvas:
+            return
+
+        self.set_current()
+        gl.glFlush()  # needs to be in try/except?
+
+        super(HeadlessContext, self).detach()
+
+        egl.eglMakeCurrent(
+            self.display_connection, 0, 0, None)
+        self.egl_surface = None
+
+    def destroy(self):
+        super(HeadlessContext, self).destroy()
+        if self.egl_context:
+            egl.eglDestroyContext(self.display_connection, self.egl_context)
+            self.egl_context = None
+
+    def flip(self):
+        if not self.egl_surface:
+            return
+
+        egl.eglSwapBuffers(self.display_connection, self.egl_surface)
diff --git a/pyglet/gl/lib.py b/pyglet/gl/lib.py
index baf2a4f..82f98c1 100644
--- a/pyglet/gl/lib.py
+++ b/pyglet/gl/lib.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/lib_agl.py b/pyglet/gl/lib_agl.py
index 0a4440a..6a1b1cc 100644
--- a/pyglet/gl/lib_agl.py
+++ b/pyglet/gl/lib_agl.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/lib_glx.py b/pyglet/gl/lib_glx.py
index 5389e5f..7086241 100644
--- a/pyglet/gl/lib_glx.py
+++ b/pyglet/gl/lib_glx.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/lib_wgl.py b/pyglet/gl/lib_wgl.py
index cbd008b..4801c21 100644
--- a/pyglet/gl/lib_wgl.py
+++ b/pyglet/gl/lib_wgl.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/wgl.py b/pyglet/gl/wgl.py
index b8ff10e..364bd46 100755
--- a/pyglet/gl/wgl.py
+++ b/pyglet/gl/wgl.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/wgl_info.py b/pyglet/gl/wgl_info.py
index dda7978..3ee7b65 100755
--- a/pyglet/gl/wgl_info.py
+++ b/pyglet/gl/wgl_info.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/wglext_arb.py b/pyglet/gl/wglext_arb.py
index 6c9f8b9..1c539f4 100644
--- a/pyglet/gl/wglext_arb.py
+++ b/pyglet/gl/wglext_arb.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/wglext_nv.py b/pyglet/gl/wglext_nv.py
index 74e1067..8fa49e7 100644
--- a/pyglet/gl/wglext_nv.py
+++ b/pyglet/gl/wglext_nv.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/win32.py b/pyglet/gl/win32.py
index a6a279a..cbaaa59 100755
--- a/pyglet/gl/win32.py
+++ b/pyglet/gl/win32.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gl/xlib.py b/pyglet/gl/xlib.py
index 5dd1ab0..41ceb3d 100644
--- a/pyglet/gl/xlib.py
+++ b/pyglet/gl/xlib.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/graphics/__init__.py b/pyglet/graphics/__init__.py
index 72c856d..5899881 100644
--- a/pyglet/graphics/__init__.py
+++ b/pyglet/graphics/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/graphics/allocation.py b/pyglet/graphics/allocation.py
index bf3ed26..da81033 100644
--- a/pyglet/graphics/allocation.py
+++ b/pyglet/graphics/allocation.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/graphics/vertexattribute.py b/pyglet/graphics/vertexattribute.py
index cdc6617..cbe3c59 100644
--- a/pyglet/graphics/vertexattribute.py
+++ b/pyglet/graphics/vertexattribute.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/graphics/vertexbuffer.py b/pyglet/graphics/vertexbuffer.py
index 350a0b2..3746fbb 100644
--- a/pyglet/graphics/vertexbuffer.py
+++ b/pyglet/graphics/vertexbuffer.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/graphics/vertexdomain.py b/pyglet/graphics/vertexdomain.py
index a797e71..06d0132 100644
--- a/pyglet/graphics/vertexdomain.py
+++ b/pyglet/graphics/vertexdomain.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gui/__init__.py b/pyglet/gui/__init__.py
index 0925275..1e51610 100644
--- a/pyglet/gui/__init__.py
+++ b/pyglet/gui/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gui/frame.py b/pyglet/gui/frame.py
index 49db90f..d38e3fa 100644
--- a/pyglet/gui/frame.py
+++ b/pyglet/gui/frame.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -32,14 +32,33 @@
 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 # ----------------------------------------------------------------------------
-import pyglet
-
-from .ninepatch import NinePatch, NinePatchGroup
 
 
 class Frame:
+    """The base Frame object, implementing a 2D spatial hash.
+
+    A `Frame` provides an efficient way to handle dispatching
+    keyboard and mouse events to Widgets. This is done by
+    implementing a 2D spatial hash. Only Widgets that are in the
+    vicinity of the mouse pointer will be passed Window events,
+    which can greatly improve efficiency when a large quantity
+    of Widgets are in use.
+    """
 
     def __init__(self, window, cell_size=64, order=0):
+        """Create an instance of a Frame.
+
+        :Parameters:
+            `window` : `~pyglet.window.Window`
+                The SpatialHash will recieve events from this Window.
+                Appropriate events will be passed on to all added Widgets.
+            `cell_size` : int
+                The cell ("bucket") size for each cell in the hash.
+                Widgets may span multiple cells.
+            `order` : int
+                Widgets use internal OrderedGroups for draw sorting.
+                This is the base value for these Groups.
+        """
         window.push_handlers(self)
         self._cell_size = cell_size
         self._cells = {}
@@ -52,71 +71,107 @@ class Frame:
         return int(x / self._cell_size), int(y / self._cell_size)
 
     def add_widget(self, widget):
-        """Insert Widget into the appropriate cell"""
+        """Add a Widget to the spatial hash."""
         min_vec, max_vec = self._hash(*widget.aabb[0:2]), self._hash(*widget.aabb[2:4])
         for i in range(min_vec[0], max_vec[0] + 1):
             for j in range(min_vec[1], max_vec[1] + 1):
                 self._cells.setdefault((i, j), set()).add(widget)
-                # TODO: return ID and track Widgets for later deletion.
         widget.update_groups(self._order)
 
+    def remove_widget(self, widget):
+        """Remove a Widget from the spatial hash."""
+        min_vec, max_vec = self._hash(*widget.aabb[0:2]), self._hash(*widget.aabb[2:4])
+        for i in range(min_vec[0], max_vec[0] + 1):
+            for j in range(min_vec[1], max_vec[1] + 1):
+                self._cells.get((i, j)).remove(widget)
+
     def on_mouse_press(self, x, y, buttons, modifiers):
         """Pass the event to any widgets within range of the mouse"""
         for widget in self._cells.get(self._hash(x, y), set()):
-            widget.dispatch_event('on_mouse_press', x, y, buttons, modifiers)
+            widget.on_mouse_press(x, y, buttons, modifiers)
             self._active_widgets.add(widget)
 
     def on_mouse_release(self, x, y, buttons, modifiers):
         """Pass the event to any widgets that are currently active"""
         for widget in self._active_widgets:
-            widget.dispatch_event('on_mouse_release', x, y, buttons, modifiers)
+            widget.on_mouse_release(x, y, buttons, modifiers)
         self._active_widgets.clear()
 
     def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
         """Pass the event to any widgets that are currently active"""
         for widget in self._active_widgets:
-            widget.dispatch_event('on_mouse_drag', x, y, dx, dy, buttons, modifiers)
+            widget.on_mouse_drag(x, y, dx, dy, buttons, modifiers)
         self._mouse_pos = x, y
 
     def on_mouse_scroll(self, x, y, index, direction):
         """Pass the event to any widgets within range of the mouse"""
         for widget in self._cells.get(self._hash(x, y), set()):
-            widget.dispatch_event('on_mouse_scroll', x, y, index, direction)
+            widget.on_mouse_scroll(x, y, index, direction)
 
     def on_mouse_motion(self, x, y, dx, dy):
         """Pass the event to any widgets within range of the mouse"""
         for widget in self._active_widgets:
-            widget.dispatch_event('on_mouse_motion', x, y, dx, dy)
+            widget.on_mouse_motion(x, y, dx, dy)
         for widget in self._cells.get(self._hash(x, y), set()):
-            widget.dispatch_event('on_mouse_motion', x, y, dx, dy)
+            widget.on_mouse_motion(x, y, dx, dy)
             self._active_widgets.add(widget)
         self._mouse_pos = x, y
 
     def on_text(self, text):
+        """Pass the event to any widgets within range of the mouse"""
         for widget in self._cells.get(self._hash(*self._mouse_pos), set()):
-            widget.dispatch_event('on_text', text)
+            widget.on_text(text)
 
     def on_text_motion(self, motion):
+        """Pass the event to any widgets within range of the mouse"""
         for widget in self._cells.get(self._hash(*self._mouse_pos), set()):
-            widget.dispatch_event('on_text_motion', motion)
+            widget.on_text_motion(motion)
 
     def on_text_motion_select(self, motion):
+        """Pass the event to any widgets within range of the mouse"""
         for widget in self._cells.get(self._hash(*self._mouse_pos), set()):
-            widget.dispatch_event('on_text_motion_select', motion)
+            widget.on_text_motion_select(motion)
 
 
-class NinePatchFrame(Frame):
+class MovableFrame(Frame):
+    """A Frame that allows Widget repositioning.
 
-    def __init__(self, x, y, width, height, window, image, group=None, batch=None, cell_size=128, order=0):
-        super().__init__(window, cell_size, order)
-        self._npatch = NinePatch(image)
-        self._npatch.get_vertices(x, y, width, height)
-        self._group = NinePatchGroup(image.get_texture(), order, group)
-        self._batch = batch or pyglet.graphics.Batch()
+    When a specified modifier key is held down, Widgets can be
+    repositioned by dragging them. Examples of modifier keys are
+    Ctrl, Alt, Shift. These are defined in the `pyglet.window.key`
+    module, and start witih `MOD_`. For example::
 
-        vertices = self._npatch.get_vertices(x, y, width, height)
-        indices = self._npatch.indices
-        tex_coords = self._npatch.tex_coords
+        from pyglet.window.key import MOD_CTRL
 
-        self._vlist = self._batch.add_indexed(16, pyglet.gl.GL_QUADS, self._group, indices,
-                                              ('v2i', vertices), ('t2f', tex_coords))
+        frame = pyglet.gui.frame.MovableFrame(mywindow, modifier=MOD_CTRL)
+
+    For more information, see the `pyglet.window.key` submodule
+    API documentation.
+    """
+
+    def __init__(self, window, order=0, modifier=0):
+        super().__init__(window, order=order)
+        self._modifier = modifier
+        self._moving_widgets = set()
+
+    def on_mouse_press(self, x, y, buttons, modifiers):
+        if self._modifier & modifiers > 0:
+            for widget in self._cells.get(self._hash(x, y), set()):
+                if widget._check_hit(x, y):
+                    self._moving_widgets.add(widget)
+            for widget in self._moving_widgets:
+                self.remove_widget(widget)
+        else:
+            super().on_mouse_press(x, y, buttons, modifiers)
+
+    def on_mouse_release(self, x, y, buttons, modifiers):
+        for widget in self._moving_widgets:
+            self.add_widget(widget)
+        self._moving_widgets.clear()
+        super().on_mouse_release(x, y, buttons, modifiers)
+
+    def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
+        for widget in self._moving_widgets:
+            wx, wy = widget.position
+            widget.position = wx + dx, wy + dy
+        super().on_mouse_drag(x, y, dx, dy, buttons, modifiers)
diff --git a/pyglet/gui/ninepatch.py b/pyglet/gui/ninepatch.py
index 089a087..d854130 100644
--- a/pyglet/gui/ninepatch.py
+++ b/pyglet/gui/ninepatch.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/gui/widgets.py b/pyglet/gui/widgets.py
index 90c1d51..14ed3b0 100644
--- a/pyglet/gui/widgets.py
+++ b/pyglet/gui/widgets.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -33,6 +33,9 @@
 # POSSIBILITY OF SUCH DAMAGE.
 # ----------------------------------------------------------------------------
 
+"""Display different types of interactive widgets.
+"""
+
 import pyglet
 
 from pyglet.event import EventDispatcher
@@ -50,33 +53,98 @@ class WidgetBase(EventDispatcher):
         self._height = height
         self._bg_group = None
         self._fg_group = None
+        self.enabled = True
 
     def update_groups(self, order):
         pass
 
     @property
     def x(self):
+        """X coordinate of the widget.
+
+        :type: int
+        """
         return self._x
 
+    @x.setter
+    def x(self, value):
+        self._x = value
+        self._update_position()
+
     @property
     def y(self):
+        """Y coordinate of the widget.
+
+        :type: int
+        """
         return self._y
 
+    @y.setter
+    def y(self, value):
+        self._y = value
+        self._update_position()
+
+    @property
+    def position(self):
+        """The x, y coordinate of the widget as a tuple.
+
+        :type: tuple(int, int)
+        """
+        return self._x, self._y
+
+    @position.setter
+    def position(self, values):
+        self._x, self._y = values
+        self._update_position()
+
     @property
     def width(self):
+        """Width of the widget.
+
+        :type: int
+        """
         return self._width
 
     @property
     def height(self):
+        """Height of the widget.
+
+        :type: int
+        """
         return self._height
 
     @property
     def aabb(self):
+        """Bounding box of the widget.
+
+        Expresesed as (x, y, x + width, y + height)
+
+        :type: (int, int, int, int)
+        """
         return self._x, self._y, self._x + self._width, self._y + self._height
 
+    @property
+    def value(self):
+        """Query or set the Widget's value.
+        
+        This property allows you to set the value of a Widget directly, without any
+        user input.  This could be used, for example, to restore Widgets to a
+        previous state, or if some event in your program is meant to naturally
+        change the same value that the Widget controls.  Note that events are not
+        dispatched when changing this property.
+        """
+        raise NotImplementedError("Value depends on control type!")
+    
+    @value.setter
+    def value(self, value):
+        raise NotImplementedError("Value depends on control type!")
+
     def _check_hit(self, x, y):
         return self._x < x < self._x + self._width and self._y < y < self._y + self._height
 
+    def _update_position(self):
+        raise NotImplementedError("Unable to reposition this Widget")
+
     def on_mouse_press(self, x, y, buttons, modifiers):
         pass
 
@@ -86,6 +154,9 @@ class WidgetBase(EventDispatcher):
     def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
         pass
 
+    def on_mouse_motion(self, x, y, dx, dy):
+        pass
+
     def on_mouse_scroll(self, x, y, mouse, direction):
         pass
 
@@ -99,19 +170,32 @@ class WidgetBase(EventDispatcher):
         pass
 
 
-WidgetBase.register_event_type('on_mouse_press')
-WidgetBase.register_event_type('on_mouse_release')
-WidgetBase.register_event_type('on_mouse_motion')
-WidgetBase.register_event_type('on_mouse_scroll')
-WidgetBase.register_event_type('on_mouse_drag')
-WidgetBase.register_event_type('on_text')
-WidgetBase.register_event_type('on_text_motion')
-WidgetBase.register_event_type('on_text_motion_select')
-
-
 class PushButton(WidgetBase):
+    """Instance of a push button.
+
+    Triggers the event 'on_press' when it is clicked by the mouse.
+    Triggers the event 'on_release' when the mouse is released.
+    """
 
     def __init__(self, x, y, pressed, depressed, hover=None, batch=None, group=None):
+        """Create a push button.
+
+        :Parameters:
+            `x` : int
+                X coordinate of the push button.
+            `y` : int
+                Y coordinate of the push button.
+            `pressed` : `~pyglet.image.AbstractImage`
+                Image to display when the button is pressed.
+            `depresseed` : `~pyglet.image.AbstractImage`
+                Image to display when the button isn't pressed.
+            `hover` : `~pyglet.image.AbstractImage`
+                Image to display when the button is being hovered over.
+            `batch` : `~pyglet.graphics.Batch`
+                Optional batch to add the push button to.
+            `group` : `~pyglet.graphics.Group`
+                Optional parent group of the push button.
+        """
         super().__init__(x, y, depressed.width, depressed.height)
         self._pressed_img = pressed
         self._depressed_img = depressed
@@ -125,30 +209,43 @@ class PushButton(WidgetBase):
 
         self._pressed = False
 
+    def _update_position(self):
+        self._sprite.position = self._x, self._y
+
+    @property
+    def value(self):
+        return self._pressed
+    
+    @value.setter
+    def value(self, value):
+        assert type(value) is bool, "This Widget's value must be True or False."
+        self._pressed = value
+        self._sprite.image = self._pressed_img if self._pressed else self._depressed_img
+
     def update_groups(self, order):
         self._sprite.group = OrderedGroup(order + 1, self._user_group)
 
     def on_mouse_press(self, x, y, buttons, modifiers):
-        if not self._check_hit(x, y):
+        if not self.enabled or not self._check_hit(x, y):
             return
         self._sprite.image = self._pressed_img
         self._pressed = True
         self.dispatch_event('on_press')
 
     def on_mouse_release(self, x, y, buttons, modifiers):
-        if not self._pressed:
+        if not self.enabled or not self._pressed:
             return
         self._sprite.image = self._hover_img if self._check_hit(x, y) else self._depressed_img
         self._pressed = False
         self.dispatch_event('on_release')
 
     def on_mouse_motion(self, x, y, dx, dy):
-        if self._pressed:
+        if not self.enabled or self._pressed:
             return
         self._sprite.image = self._hover_img if self._check_hit(x, y) else self._depressed_img
 
     def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
-        if self._pressed:
+        if not self.enabled or self._pressed:
             return
         self._sprite.image = self._hover_img if self._check_hit(x, y) else self._depressed_img
 
@@ -158,19 +255,23 @@ PushButton.register_event_type('on_release')
 
 
 class ToggleButton(PushButton):
+    """Instance of a toggle button.
+
+    Triggers the event 'on_toggle' when the mouse is pressed or released.
+    """
 
     def _get_release_image(self, x, y):
         return self._hover_img if self._check_hit(x, y) else self._depressed_img
 
     def on_mouse_press(self, x, y, buttons, modifiers):
-        if not self._check_hit(x, y):
+        if not self.enabled or not self._check_hit(x, y):
             return
         self._pressed = not self._pressed
         self._sprite.image = self._pressed_img if self._pressed else self._get_release_image(x, y)
         self.dispatch_event('on_toggle', self._pressed)
 
     def on_mouse_release(self, x, y, buttons, modifiers):
-        if self._pressed:
+        if not self.enabled or self._pressed:
             return
         self._sprite.image = self._get_release_image(x, y)
 
@@ -179,8 +280,33 @@ ToggleButton.register_event_type('on_toggle')
 
 
 class Slider(WidgetBase):
+    """Instance of a slider made of a base and a knob image.
+
+    Triggers the event 'on_change' when the knob position is changed.
+    The knob position can be changed by dragging with the mouse, or
+    scrolling the mouse wheel.
+    """
 
     def __init__(self, x, y, base, knob, edge=0, batch=None, group=None):
+        """Create a slider.
+
+        :Parameters:
+            `x` : int
+                X coordinate of the slider.
+            `y` : int
+                Y coordinate of the slider.
+            `base` : `~pyglet.image.AbstractImage`
+                Image to display as the background to the slider.
+            `knob` : `~pyglet.image.AbstractImage`
+                Knob that moves to show the position of the slider.
+            `edge` : int
+                Pixels from the maximum and minimum position of the slider,
+                to the edge of the base image.
+            `batch` : `~pyglet.graphics.Batch`
+                Optional batch to add the slider to.
+            `group` : `~pyglet.graphics.Group`
+                Optional parent group of the slider.
+        """
         super().__init__(x, y, base.width, knob.height)
         self._edge = edge
         self._base_img = base
@@ -201,6 +327,17 @@ class Slider(WidgetBase):
         self._value = 0
         self._in_update = False
 
+    @property
+    def value(self):
+        return self._value
+
+    @value.setter
+    def value(self, value):
+        assert type(value) in (int, float), "This Widget's value must be an int or float."
+        self._value = value
+        x = (self._max_knob_x - self._min_knob_x) * value / 100 + self._min_knob_x + self._half_knob_width
+        self._knob_spr.x = max(self._min_knob_x, min(x - self._half_knob_width, self._max_knob_x))
+
     def update_groups(self, order):
         self._base_spr.group = OrderedGroup(order + 1, self._user_group)
         self._knob_spr.group = OrderedGroup(order + 2, self._user_group)
@@ -230,19 +367,27 @@ class Slider(WidgetBase):
         self.dispatch_event('on_change', self._value)
 
     def on_mouse_press(self, x, y, buttons, modifiers):
+        if not self.enabled:
+            return
         if self._check_hit(x, y):
             self._in_update = True
             self._update_knob(x)
 
     def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
+        if not self.enabled:
+            return
         if self._in_update:
             self._update_knob(x)
 
     def on_mouse_scroll(self, x, y, mouse, direction):
+        if not self.enabled:
+            return
         if self._check_hit(x, y):
             self._update_knob(self._knob_spr.x + self._half_knob_width + direction)
 
     def on_mouse_release(self, x, y, buttons, modifiers):
+        if not self.enabled:
+            return
         self._in_update = False
 
 
@@ -250,10 +395,38 @@ Slider.register_event_type('on_change')
 
 
 class TextEntry(WidgetBase):
-
-    def __init__(self, text, x, y, width, color=(255, 255, 255, 255), batch=None, group=None):
+    """Instance of a text entry widget.
+
+    Allows the user to enter and submit text.
+    """
+
+    def __init__(self, text, x, y, width,
+                 color=(255, 255, 255, 255), text_color=(0, 0, 0, 255), caret_color=(0, 0, 0),
+                 batch=None, group=None):
+        """Create a text entry widget.
+
+        :Parameters:
+            `text` : str
+                Initial text to display.
+            `x` : int
+                X coordinate of the text entry widget.
+            `y` : int
+                Y coordinate of the text entry widget.
+            `width` : int
+                The width of the text entry widget.
+            `color` : (int, int, int, int)
+                The color of the outline box in RGBA format.
+            `text_color` : (int, int, int, int)
+                The color of the text in RGBA format.
+            `text_color` : (int, int, int)
+                The color of the caret in RGB format.
+            `batch` : `~pyglet.graphics.Batch`
+                Optional batch to add the text entry widget to.
+            `group` : `~pyglet.graphics.Group`
+                Optional parent group of text entry widget.
+        """
         self._doc = pyglet.text.document.UnformattedDocument(text)
-        self._doc.set_style(0, len(self._doc.text), dict(color=(0, 0, 0, 255)))
+        self._doc.set_style(0, len(self._doc.text), dict(color=text_color))
         font = self._doc.get_font()
         height = font.ascent - font.descent
 
@@ -262,7 +435,7 @@ class TextEntry(WidgetBase):
         fg_group = OrderedGroup(1, parent=group)
 
         # Rectangular outline with 2-pixel pad:
-        p = 2
+        self._pad = p = 2
         self._outline = pyglet.shapes.Rectangle(x-p, y-p, width+p+p, height+p+p, color[:3], batch, bg_group)
         self._outline.opacity = color[3]
 
@@ -270,13 +443,26 @@ class TextEntry(WidgetBase):
         self._layout = IncrementalTextLayout(self._doc, width, height, multiline=False, batch=batch, group=fg_group)
         self._layout.x = x
         self._layout.y = y
-        self._caret = Caret(self._layout)
+        self._caret = Caret(self._layout, color=caret_color)
         self._caret.visible = False
 
         self._focus = False
 
         super().__init__(x, y, width, height)
 
+    def _update_position(self):
+        self._layout.position = self._x, self._y
+        self._outline.position = self._x - self._pad, self._y - self._pad
+
+    @property
+    def value(self):
+        return self._doc.text
+
+    @value.setter
+    def value(self, value):
+        assert type(value) is str, "This Widget's value must be a string."
+        self._doc.text = value
+
     def _check_hit(self, x, y):
         return self._x < x < self._x + self._width and self._y < y < self._y + self._height
 
@@ -289,19 +475,27 @@ class TextEntry(WidgetBase):
         self._layout.group = OrderedGroup(order + 2, self._user_group)
 
     def on_mouse_motion(self, x, y, dx, dy):
+        if not self.enabled:
+            return
         if not self._check_hit(x, y):
             self._set_focus(False)
 
     def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
+        if not self.enabled:
+            return
         if self._focus:
             self._caret.on_mouse_drag(x, y, dx, dy, buttons, modifiers)
 
     def on_mouse_press(self, x, y, buttons, modifiers):
+        if not self.enabled:
+            return
         if self._check_hit(x, y):
             self._set_focus(True)
             self._caret.on_mouse_press(x, y, buttons, modifiers)
 
     def on_text(self, text):
+        if not self.enabled:
+            return
         if self._focus:
             if text in ('\r', '\n'):
                 self.dispatch_event('on_commit', self._layout.document.text)
@@ -310,14 +504,20 @@ class TextEntry(WidgetBase):
             self._caret.on_text(text)
 
     def on_text_motion(self, motion):
+        if not self.enabled:
+            return
         if self._focus:
             self._caret.on_text_motion(motion)
 
     def on_text_motion_select(self, motion):
+        if not self.enabled:
+            return
         if self._focus:
             self._caret.on_text_motion_select(motion)
 
     def on_commit(self, text):
+        if not self.enabled:
+            return
         """Text has been commited via Enter/Return key."""
 
 
diff --git a/pyglet/image/__init__.py b/pyglet/image/__init__.py
index f2952ad..ce791c4 100644
--- a/pyglet/image/__init__.py
+++ b/pyglet/image/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -135,9 +135,10 @@ from ctypes import *
 from io import open, BytesIO
 from functools import lru_cache
 
+import pyglet
+
 from pyglet.gl import *
 from pyglet.gl import gl_info
-from pyglet.window import *
 from pyglet.util import asbytes
 
 from .codecs import ImageEncodeException, ImageDecodeException
@@ -956,7 +957,7 @@ class ImageData(AbstractImage):
                 format, type = self._get_gl_format_and_type(data_format)
 
         # Workaround: don't use GL_UNPACK_ROW_LENGTH
-        if gl.current_context._workaround_unpack_row_length:
+        if pyglet.gl.current_context._workaround_unpack_row_length:
             data_pitch = self.width * len(data_format)
 
         # Get data in required format (hopefully will be the same format it's
@@ -1392,7 +1393,7 @@ class Texture(AbstractImage):
         super(Texture, self).__init__(width, height)
         self.target = target
         self.id = id
-        self._context = gl.current_context
+        self._context = pyglet.gl.current_context
 
     def __del__(self):
         try:
@@ -2003,7 +2004,7 @@ def get_buffer_manager():
     
     :rtype: :py:class:`~pyglet.image.BufferManager`
     """
-    context = gl.current_context
+    context = pyglet.gl.current_context
     if not hasattr(context, 'image_buffer_manager'):
         context.image_buffer_manager = BufferManager()
     return context.image_buffer_manager
diff --git a/pyglet/image/animation.py b/pyglet/image/animation.py
index 9d41dd6..a6e86f4 100644
--- a/pyglet/image/animation.py
+++ b/pyglet/image/animation.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/image/atlas.py b/pyglet/image/atlas.py
index b890ef2..195a594 100644
--- a/pyglet/image/atlas.py
+++ b/pyglet/image/atlas.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/image/codecs/__init__.py b/pyglet/image/codecs/__init__.py
index a416117..c24a56a 100644
--- a/pyglet/image/codecs/__init__.py
+++ b/pyglet/image/codecs/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/image/codecs/bmp.py b/pyglet/image/codecs/bmp.py
index 5c9763f..f71ae9f 100644
--- a/pyglet/image/codecs/bmp.py
+++ b/pyglet/image/codecs/bmp.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/image/codecs/dds.py b/pyglet/image/codecs/dds.py
index 89f3035..92a7e20 100644
--- a/pyglet/image/codecs/dds.py
+++ b/pyglet/image/codecs/dds.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/image/codecs/gdiplus.py b/pyglet/image/codecs/gdiplus.py
index ed5ce42..fcdad66 100644
--- a/pyglet/image/codecs/gdiplus.py
+++ b/pyglet/image/codecs/gdiplus.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/image/codecs/gdkpixbuf2.py b/pyglet/image/codecs/gdkpixbuf2.py
index 2a61631..91b45ed 100644
--- a/pyglet/image/codecs/gdkpixbuf2.py
+++ b/pyglet/image/codecs/gdkpixbuf2.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/image/codecs/gif.py b/pyglet/image/codecs/gif.py
index 5e4e46e..19d2c8b 100644
--- a/pyglet/image/codecs/gif.py
+++ b/pyglet/image/codecs/gif.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/image/codecs/pil.py b/pyglet/image/codecs/pil.py
index 32b42e6..067870d 100644
--- a/pyglet/image/codecs/pil.py
+++ b/pyglet/image/codecs/pil.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/image/codecs/png.py b/pyglet/image/codecs/png.py
index 8bd79e2..7e4f615 100644
--- a/pyglet/image/codecs/png.py
+++ b/pyglet/image/codecs/png.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -39,8 +39,8 @@
 import array
 import itertools
 
-from pyglet.image import *
-from pyglet.image.codecs import *
+from pyglet.image import ImageData, ImageDecodeException
+from pyglet.image.codecs import ImageDecoder, ImageEncoder
 
 import pyglet.extlibs.png as pypng
 
@@ -54,8 +54,7 @@ class PNGImageDecoder(ImageDecoder):
             reader = pypng.Reader(file=file)
             width, height, pixels, metadata = reader.asDirect()
         except Exception as e:
-            raise ImageDecodeException(
-                'PyPNG cannot read %r: %s' % (filename or file, e))
+            raise ImageDecodeException('PyPNG cannot read %r: %s' % (filename or file, e))
 
         if metadata['greyscale']:
             if metadata['alpha']:
@@ -95,10 +94,10 @@ class PNGImageEncoder(ImageEncoder):
 
         image.pitch = -(image.width * len(image.format))
 
-        writer = pypng.Writer(image.width, image.height, bytes_per_sample=1, greyscale=greyscale, alpha=has_alpha)
+        writer = pypng.Writer(image.width, image.height, greyscale=greyscale, alpha=has_alpha)
 
         data = array.array('B')
-        data.fromstring(image.get_data(image.format, image.pitch))
+        data.frombytes(image.get_data(image.format, image.pitch))
 
         writer.write_array(file, data)
 
diff --git a/pyglet/image/codecs/quartz.py b/pyglet/image/codecs/quartz.py
index b080404..50e434f 100644
--- a/pyglet/image/codecs/quartz.py
+++ b/pyglet/image/codecs/quartz.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/image/codecs/s3tc.py b/pyglet/image/codecs/s3tc.py
index 4093381..61b04ee 100644
--- a/pyglet/image/codecs/s3tc.py
+++ b/pyglet/image/codecs/s3tc.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/image/codecs/wic.py b/pyglet/image/codecs/wic.py
index 7e9f56e..97b7727 100644
--- a/pyglet/image/codecs/wic.py
+++ b/pyglet/image/codecs/wic.py
@@ -48,14 +48,20 @@ WICBitmapDitherTypeErrorDiffusion = 0x8
 WICBITMAPDITHERTYPE_FORCE_DWORD = 0x7fffffff
 WICBITMAPTRANSFORMOPTIONS_FORCE_DWORD = 0x7fffffff
 
-
 WICDecodeOptions = UINT
 WICDecodeMetadataCacheOnDemand = 0
 WICDecodeMetadataCacheOnLoad = 0x1
 WICMETADATACACHEOPTION_FORCE_DWORD = 0x7fffffff
 
+WICBitmapEncoderCacheOption = UINT
+WICBitmapEncoderCacheInMemory = 0x0
+WICBitmapEncoderCacheTempFile = 0x1
+WICBitmapEncoderNoCache = 0x2
+WICBITMAPENCODERCACHEOPTION_FORCE_DWORD = 0x7fffffff
+
 # Different pixel formats.
 REFWICPixelFormatGUID = com.GUID
+GUID_WICPixelFormatDontCare = com.GUID(0x6fddc324,0x4e03,0x4bfe,0xb1,0x85,0x3d,0x77,0x76,0x8d,0xc9,0x00)
 GUID_WICPixelFormat1bppIndexed = com.GUID(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x01)
 GUID_WICPixelFormat2bppIndexed = com.GUID(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x02)
 GUID_WICPixelFormat4bppIndexed = com.GUID(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x03)
@@ -74,12 +80,155 @@ GUID_WICPixelFormat24bppRGB = com.GUID(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0
 GUID_WICPixelFormat32bppBGR = com.GUID(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x0e)
 GUID_WICPixelFormat32bppBGRA = com.GUID(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x0f)
 GUID_WICPixelFormat32bppPBGRA = com.GUID(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x10)
-GUID_WICPixelFormat32bppRGB = com.GUID(0xd98c6b95, 0x3efe, 0x47d6, 0xbb, 0x25, 0xeb, 0x17, 0x48, 0xab, 0x0c, 0xf1)  # 7 platform update?
+GUID_WICPixelFormat32bppRGB = com.GUID(0xd98c6b95, 0x3efe, 0x47d6, 0xbb, 0x25, 0xeb, 0x17, 0x48, 0xab, 0x0c,  0xf1)  # 7 platform update?
 GUID_WICPixelFormat32bppRGBA = com.GUID(0xf5c7ad2d, 0x6a8d, 0x43dd, 0xa7, 0xa8, 0xa2, 0x99, 0x35, 0x26, 0x1a, 0xe9)
 GUID_WICPixelFormat32bppPRGBA = com.GUID(0x3cc4a650, 0xa527, 0x4d37, 0xa9, 0x16, 0x31, 0x42, 0xc7, 0xeb, 0xed, 0xba)
 GUID_WICPixelFormat48bppRGB = com.GUID(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x15)
 GUID_WICPixelFormat48bppBGR = com.GUID(0xe605a384, 0xb468, 0x46ce, 0xbb, 0x2e, 0x36, 0xf1, 0x80, 0xe6, 0x43, 0x13)
 
+GUID_ContainerFormatBmp = com.GUID(0x0af1d87e, 0xfcfe, 0x4188, 0xbd, 0xeb, 0xa7, 0x90, 0x64, 0x71, 0xcb, 0xe3)
+GUID_ContainerFormatPng = com.GUID(0x1b7cfaf4, 0x713f, 0x473c, 0xbb, 0xcd, 0x61, 0x37, 0x42, 0x5f, 0xae, 0xaf)
+GUID_ContainerFormatIco = com.GUID(0xa3a860c4, 0x338f, 0x4c17, 0x91, 0x9a, 0xfb, 0xa4, 0xb5, 0x62, 0x8f, 0x21)
+GUID_ContainerFormatJpeg = com.GUID(0x19e4a5aa, 0x5662, 0x4fc5, 0xa0, 0xc0, 0x17, 0x58, 0x02, 0x8e, 0x10, 0x57)
+GUID_ContainerFormatTiff = com.GUID(0x163bcc30, 0xe2e9, 0x4f0b, 0x96, 0x1d, 0xa3, 0xe9, 0xfd, 0xb7, 0x88, 0xa3)
+GUID_ContainerFormatGif = com.GUID(0x1f8a5601, 0x7d4d, 0x4cbd, 0x9c, 0x82, 0x1b, 0xc8, 0xd4, 0xee, 0xb9, 0xa5)
+GUID_ContainerFormatWmp = com.GUID(0x57a37caa, 0x367a, 0x4540, 0x91, 0x6b, 0xf1, 0x83, 0xc5, 0x09, 0x3a, 0x4b)
+
+
+class IPropertyBag2(com.pIUnknown):
+    _methods_ = [
+        ('Read',
+         com.STDMETHOD()),
+        ('Write',
+         com.STDMETHOD()),
+        ('CountProperties',
+         com.STDMETHOD()),
+        ('GetPropertyInfo',
+         com.STDMETHOD()),
+        ('LoadObject',
+         com.STDMETHOD())
+    ]
+
+
+# class PROPBAG2(Structure):
+#     _fields_ = [
+#         ('dwType', DWORD),
+#         ('vt', VARTYPE),
+#         ('cfType', CLIPFORMAT),
+#         ('dwHint', DWORD),
+#         ('pstrName', LPOLESTR),
+#         ('clsid', CLSID)
+#     ]
+
+class IWICPalette(com.pIUnknown):
+    _methods_ = [
+        ('InitializePredefined',
+         com.STDMETHOD()),
+        ('InitializeCustom',
+         com.STDMETHOD()),
+        ('InitializeFromBitmap',
+         com.STDMETHOD()),
+        ('InitializeFromPalette',
+         com.STDMETHOD()),
+        ('GetType',
+         com.STDMETHOD()),
+        ('GetColorCount',
+         com.STDMETHOD()),
+        ('GetColors',
+         com.STDMETHOD()),
+        ('IsBlackWhite',
+         com.STDMETHOD()),
+        ('IsGrayscale',
+         com.STDMETHOD()),
+        ('HasAlpha',
+         com.STDMETHOD()),
+    ]
+class IWICStream(com.pIUnknown):
+    _methods_ = [
+        ('Read',
+         com.STDMETHOD()),
+        ('Write',
+         com.STDMETHOD()),
+        ('Seek',
+         com.STDMETHOD()),
+        ('SetSize',
+         com.STDMETHOD()),
+        ('CopyTo',
+         com.STDMETHOD()),
+        ('Commit',
+         com.STDMETHOD()),
+        ('Revert',
+         com.STDMETHOD()),
+        ('LockRegion',
+         com.STDMETHOD()),
+        ('UnlockRegion',
+         com.STDMETHOD()),
+        ('Stat',
+         com.STDMETHOD()),
+        ('Clone',
+         com.STDMETHOD()),
+        ('InitializeFromIStream',
+         com.STDMETHOD()),
+        ('InitializeFromFilename',
+         com.STDMETHOD(LPCWSTR, DWORD)),
+        ('InitializeFromMemory',
+         com.STDMETHOD(c_void_p, DWORD)),
+        ('InitializeFromIStreamRegion',
+         com.STDMETHOD()),
+    ]
+
+
+class IWICBitmapFrameEncode(com.pIUnknown):
+    _methods_ = [
+        ('Initialize',
+         com.STDMETHOD(IPropertyBag2)),
+        ('SetSize',
+         com.STDMETHOD(UINT, UINT)),
+        ('SetResolution',
+         com.STDMETHOD()),
+        ('SetPixelFormat',
+         com.STDMETHOD(REFWICPixelFormatGUID)),
+        ('SetColorContexts',
+         com.STDMETHOD()),
+        ('SetPalette',
+         com.STDMETHOD(IWICPalette)),
+        ('SetThumbnail',
+         com.STDMETHOD()),
+        ('WritePixels',
+         com.STDMETHOD(UINT, UINT, UINT, POINTER(BYTE))),
+        ('WriteSource',
+         com.STDMETHOD()),
+        ('Commit',
+         com.STDMETHOD()),
+        ('GetMetadataQueryWriter',
+         com.STDMETHOD())
+    ]
+
+
+class IWICBitmapEncoder(com.pIUnknown):
+    _methods_ = [
+        ('Initialize',
+         com.STDMETHOD(IWICStream, WICBitmapEncoderCacheOption)),
+        ('GetContainerFormat',
+         com.STDMETHOD()),
+        ('GetEncoderInfo',
+         com.STDMETHOD()),
+        ('SetColorContexts',
+         com.STDMETHOD()),
+        ('SetPalette',
+         com.STDMETHOD()),
+        ('SetThumbnail',
+         com.STDMETHOD()),
+        ('SetPreview',
+         com.STDMETHOD()),
+        ('CreateNewFrame',
+         com.STDMETHOD(POINTER(IWICBitmapFrameEncode), POINTER(IPropertyBag2))),
+        ('Commit',
+         com.STDMETHOD()),
+        ('GetMetadataQueryWriter',
+         com.STDMETHOD())
+    ]
+
 
 class IWICComponentInfo(com.pIUnknown):
     _methods_ = [
@@ -135,7 +284,8 @@ class IWICBitmapSource(com.pIUnknown):
 class IWICFormatConverter(IWICBitmapSource, com.pIUnknown):
     _methods_ = [
         ('Initialize',
-         com.STDMETHOD(IWICBitmapSource, POINTER(REFWICPixelFormatGUID), WICBitmapDitherType, c_void_p, DOUBLE, WICBitmapPaletteType)),
+         com.STDMETHOD(IWICBitmapSource, POINTER(REFWICPixelFormatGUID), WICBitmapDitherType, c_void_p, DOUBLE,
+                       WICBitmapPaletteType)),
         ('CanConvert',
          com.STDMETHOD(POINTER(REFWICPixelFormatGUID), POINTER(REFWICPixelFormatGUID), POINTER(BOOL))),
     ]
@@ -234,9 +384,9 @@ class IWICImagingFactory(com.pIUnknown):
         ('CreateDecoder',
          com.STDMETHOD()),
         ('CreateEncoder',
-         com.STDMETHOD()),
+         com.STDMETHOD(com.GUID, POINTER(com.GUID), POINTER(IWICBitmapEncoder))),
         ('CreatePalette',
-         com.STDMETHOD()),
+         com.STDMETHOD(POINTER(IWICPalette))),
         ('CreateFormatConverter',
          com.STDMETHOD(POINTER(IWICFormatConverter))),
         ('CreateBitmapScaler',
@@ -246,19 +396,19 @@ class IWICImagingFactory(com.pIUnknown):
         ('CreateBitmapFlipRotator',
          com.STDMETHOD(POINTER(IWICBitmapFlipRotator))),
         ('CreateStream',
-         com.STDMETHOD()),
+         com.STDMETHOD(POINTER(IWICStream))),
         ('CreateColorContext',
          com.STDMETHOD()),
         ('CreateColorTransformer',
          com.STDMETHOD()),
         ('CreateBitmap',
-         com.STDMETHOD(UINT, UINT, REFWICPixelFormatGUID, WICBitmapCreateCacheOption, POINTER(IWICBitmap))),
+         com.STDMETHOD(UINT, UINT, POINTER(REFWICPixelFormatGUID), WICBitmapCreateCacheOption, POINTER(IWICBitmap))),
         ('CreateBitmapFromSource',
          com.STDMETHOD()),
         ('CreateBitmapFromSourceRect',
          com.STDMETHOD()),
         ('CreateBitmapFromMemory',
-         com.STDMETHOD()),
+         com.STDMETHOD(UINT, UINT, REFWICPixelFormatGUID, UINT, UINT, POINTER(BYTE), POINTER(IWICBitmap))),
         ('CreateBitmapFromHBITMAP',
          com.STDMETHOD()),
         ('CreateBitmapFromHICON',
@@ -276,23 +426,27 @@ class IWICImagingFactory(com.pIUnknown):
     ]
 
 
+_factory = IWICImagingFactory()
+
+try:
+    ole32.CoInitializeEx(None, COINIT_MULTITHREADED)
+except OSError as err:
+    warnings.warn(str(err))
+
+ole32.CoCreateInstance(CLSID_WICImagingFactory,
+                       None,
+                       CLSCTX_INPROC_SERVER,
+                       IID_IWICImagingFactory,
+                       byref(_factory))
+
+
 class WICDecoder(ImageDecoder):
     """Windows Imaging Component.
     This decoder is a replacement for GDI and GDI+ starting with Windows 7 with more features up to Windows 10."""
+
     def __init__(self):
         super(ImageDecoder, self).__init__()
-        self._factory = IWICImagingFactory()
-
-        try:
-            ole32.CoInitializeEx(None, COINIT_MULTITHREADED)
-        except OSError as err:
-            warnings.warn(str(err))
-
-        ole32.CoCreateInstance(CLSID_WICImagingFactory,
-                               None,
-                               CLSCTX_INPROC_SERVER,
-                               IID_IWICImagingFactory,
-                               byref(self._factory))
+        self._factory = _factory
 
     def get_file_extensions(self):
         return ['.bmp', '.jpg', '.jpeg', '.png', '.tif', '.tiff', '.ico', '.jxr', '.hdp', '.wdp']
@@ -324,7 +478,7 @@ class WICDecoder(ImageDecoder):
         bitmap_decoder.GetFrame(frame_index, byref(bitmap))
         return bitmap
 
-    def _get_image(self, bitmap, target_fmt=GUID_WICPixelFormat32bppBGRA):
+    def get_image(self, bitmap, target_fmt=GUID_WICPixelFormat32bppBGRA):
         """Get's image from bitmap, specifying target format, bitmap is released before returning."""
         width = UINT()
         height = UINT()
@@ -385,7 +539,7 @@ class WICDecoder(ImageDecoder):
     def decode(self, file, filename):
         bitmap_decoder, stream = self._load_bitmap_decoder(file, filename)
         bitmap = self._get_bitmap_frame(bitmap_decoder, 0)
-        image = self._get_image(bitmap)
+        image = self.get_image(bitmap)
         self._delete_bitmap_decoder(bitmap_decoder, stream)
         return image
 
@@ -411,5 +565,92 @@ def get_decoders():
     return [WICDecoder()]
 
 
+extension_to_container = {
+    '.bmp': GUID_ContainerFormatBmp,
+    '.jpg': GUID_ContainerFormatJpeg,
+    '.jpeg': GUID_ContainerFormatJpeg,
+    '.tif': GUID_ContainerFormatTiff,
+    '.tiff': GUID_ContainerFormatTiff,
+    '.wmp': GUID_ContainerFormatWmp,
+    '.jxr': GUID_ContainerFormatWmp,
+    '.wdp': GUID_ContainerFormatWmp,
+    '.png': GUID_ContainerFormatPng,
+}
+
+
+class WICEncoder(ImageEncoder):
+    def get_file_extensions(self):
+        return [ext for ext in extension_to_container]
+
+    def encode(self, image, file, filename):
+        image = image.get_image_data()
+
+        stream = IWICStream()
+        encoder = IWICBitmapEncoder()
+        frame = IWICBitmapFrameEncode()
+        property_bag = IPropertyBag2()
+
+        ext = (filename and os.path.splitext(filename)[1]) or '.png'
+
+        # Choose container based on extension. Default to PNG.
+        container = extension_to_container.get(ext, GUID_ContainerFormatPng)
+
+        _factory.CreateStream(byref(stream))
+        # https://docs.microsoft.com/en-us/windows/win32/wic/-wic-codec-native-pixel-formats#native-image-formats
+        if container == GUID_ContainerFormatJpeg:
+            # Expects BGR, no transparency available. Hard coded.
+            fmt = 'BGR'
+            default_format = GUID_WICPixelFormat24bppBGR
+        else:
+            # Windows encodes in BGRA.
+            if len(image.format) == 3:
+                fmt = 'BGR'
+                default_format = GUID_WICPixelFormat24bppBGR
+            else:
+                fmt = 'BGRA'
+                default_format = GUID_WICPixelFormat32bppBGRA
+
+        pitch = image.width * len(fmt)
+
+        image_data = image.get_data(fmt, -pitch)
+
+        size = len(image_data)
+
+        if file:
+            buf = create_string_buffer(size)
+
+            stream.InitializeFromMemory(byref(buf), size)
+        else:
+            stream.InitializeFromFilename(filename, GENERIC_WRITE)
+
+        _factory.CreateEncoder(container, None, byref(encoder))
+
+        encoder.Initialize(stream, WICBitmapEncoderNoCache)
+
+        encoder.CreateNewFrame(byref(frame), byref(property_bag))
+
+        frame.Initialize(property_bag)
+
+        frame.SetSize(image.width, image.height)
+
+        frame.SetPixelFormat(default_format)
+
+        data = (c_byte * size).from_buffer(bytearray(image_data))
+
+        frame.WritePixels(image.height, abs(image.pitch), size, data)
+
+        frame.Commit()
+
+        encoder.Commit()
+
+        if file:
+            file.write(buf)
+
+        encoder.Release()
+        frame.Release()
+        property_bag.Release()
+        stream.Release()
+
+
 def get_encoders():
-    return []
+    return [WICEncoder()]
diff --git a/pyglet/info.py b/pyglet/info.py
index 0a5da19..277fe30 100644
--- a/pyglet/info.py
+++ b/pyglet/info.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/input/__init__.py b/pyglet/input/__init__.py
index b7c0361..a557858 100644
--- a/pyglet/input/__init__.py
+++ b/pyglet/input/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/input/base.py b/pyglet/input/base.py
index d898619..1ad15b5 100644
--- a/pyglet/input/base.py
+++ b/pyglet/input/base.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -698,7 +698,7 @@ class TabletCanvas(EventDispatcher):
             :event:
             """
 
-        def on_motion(self, cursor, x, y, pressure):
+        def on_motion(self, cursor, x, y, pressure, tilt_x, tilt_y):
             """The cursor moved on the tablet surface.
 
             If `pressure` is 0, then the cursor is actually hovering above the
diff --git a/pyglet/input/darwin_hid.py b/pyglet/input/darwin_hid.py
index ce72dbb..0c55bdc 100644
--- a/pyglet/input/darwin_hid.py
+++ b/pyglet/input/darwin_hid.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/input/directinput.py b/pyglet/input/directinput.py
index 00f137b..6b6dece 100755
--- a/pyglet/input/directinput.py
+++ b/pyglet/input/directinput.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -236,7 +236,8 @@ def get_devices(display=None):
 def _create_joystick(device):
     if device._type in (dinput.DI8DEVTYPE_JOYSTICK,
                         dinput.DI8DEVTYPE_1STPERSON,
-                        dinput.DI8DEVTYPE_GAMEPAD):
+                        dinput.DI8DEVTYPE_GAMEPAD,
+                        dinput.DI8DEVTYPE_SUPPLEMENTAL):
         return base.Joystick(device)
 
 
diff --git a/pyglet/input/evdev.py b/pyglet/input/evdev.py
index 2166bdf..c4dab1c 100644
--- a/pyglet/input/evdev.py
+++ b/pyglet/input/evdev.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/input/evdev_constants.py b/pyglet/input/evdev_constants.py
index f0f5384..8318067 100644
--- a/pyglet/input/evdev_constants.py
+++ b/pyglet/input/evdev_constants.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/input/wintab.py b/pyglet/input/wintab.py
index c6a6987..cca91d0 100755
--- a/pyglet/input/wintab.py
+++ b/pyglet/input/wintab.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/input/x11_xinput.py b/pyglet/input/x11_xinput.py
index 1bcfda6..ae5f6d7 100644
--- a/pyglet/input/x11_xinput.py
+++ b/pyglet/input/x11_xinput.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/input/x11_xinput_tablet.py b/pyglet/input/x11_xinput_tablet.py
index dd17969..c4f093c 100644
--- a/pyglet/input/x11_xinput_tablet.py
+++ b/pyglet/input/x11_xinput_tablet.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -113,7 +113,7 @@ class XInputTabletCursor(TabletCursor):
 def get_tablets(display=None):
     # Each cursor appears as a separate xinput device; find devices that look
     # like Wacom tablet cursors and amalgamate them into a single tablet. 
-    valid_names = ('stylus', 'cursor', 'eraser', 'wacom', 'pen', 'pad')
+    valid_names = ('stylus', 'cursor', 'eraser', 'pen', 'pad')
     cursors = []
     devices = get_devices(display)
     for device in devices:
diff --git a/pyglet/lib.py b/pyglet/lib.py
index 1a3f7c0..2fa79cc 100644
--- a/pyglet/lib.py
+++ b/pyglet/lib.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/libs/darwin/__init__.py b/pyglet/libs/darwin/__init__.py
index 61cb53a..9815575 100644
--- a/pyglet/libs/darwin/__init__.py
+++ b/pyglet/libs/darwin/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/libs/darwin/quartzkey.py b/pyglet/libs/darwin/quartzkey.py
index c4cf728..fea3006 100644
--- a/pyglet/libs/darwin/quartzkey.py
+++ b/pyglet/libs/darwin/quartzkey.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/libs/egl/__init__.py b/pyglet/libs/egl/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pyglet/libs/egl/egl.py b/pyglet/libs/egl/egl.py
new file mode 100644
index 0000000..19f9d63
--- /dev/null
+++ b/pyglet/libs/egl/egl.py
@@ -0,0 +1,596 @@
+'''Wrapper for /usr/include/EGL/egl
+
+Generated with:
+wrap.py -o lib_egl.py /usr/include/EGL/egl.h
+
+Do not modify this file.
+'''
+
+__docformat__ =  'restructuredtext'
+__version__ = '$Id$'
+
+import ctypes
+from ctypes import *
+
+import pyglet.lib
+
+_lib = pyglet.lib.load_library('EGL')
+
+_int_types = (c_int16, c_int32)
+if hasattr(ctypes, 'c_int64'):
+    # Some builds of ctypes apparently do not have c_int64
+    # defined; it's a pretty good bet that these builds do not
+    # have 64-bit pointers.
+    _int_types += (ctypes.c_int64,)
+for t in _int_types:
+    if sizeof(t) == sizeof(c_size_t):
+        c_ptrdiff_t = t
+
+class c_void(Structure):
+    # c_void_p is a buggy return type, converting to int, so
+    # POINTER(None) == c_void_p is actually written as
+    # POINTER(c_void), so it can be treated as a real pointer.
+    _fields_ = [('dummy', c_int)]
+
+
+
+__egl_h_ = 1 	# /usr/include/EGL/egl.h:2
+EGL_EGL_PROTOTYPES = 1 	# /usr/include/EGL/egl.h:42
+EGL_VERSION_1_0 = 1 	# /usr/include/EGL/egl.h:57
+EGLBoolean = c_uint 	# /usr/include/EGL/egl.h:58
+EGLDisplay = POINTER(None) 	# /usr/include/EGL/egl.h:59
+EGLConfig = POINTER(None) 	# /usr/include/EGL/egl.h:62
+EGLSurface = POINTER(None) 	# /usr/include/EGL/egl.h:63
+EGLContext = POINTER(None) 	# /usr/include/EGL/egl.h:64
+__eglMustCastToProperFunctionPointerType = CFUNCTYPE(None) 	# /usr/include/EGL/egl.h:65
+EGL_ALPHA_SIZE = 12321 	# /usr/include/EGL/egl.h:66
+EGL_BAD_ACCESS = 12290 	# /usr/include/EGL/egl.h:67
+EGL_BAD_ALLOC = 12291 	# /usr/include/EGL/egl.h:68
+EGL_BAD_ATTRIBUTE = 12292 	# /usr/include/EGL/egl.h:69
+EGL_BAD_CONFIG = 12293 	# /usr/include/EGL/egl.h:70
+EGL_BAD_CONTEXT = 12294 	# /usr/include/EGL/egl.h:71
+EGL_BAD_CURRENT_SURFACE = 12295 	# /usr/include/EGL/egl.h:72
+EGL_BAD_DISPLAY = 12296 	# /usr/include/EGL/egl.h:73
+EGL_BAD_MATCH = 12297 	# /usr/include/EGL/egl.h:74
+EGL_BAD_NATIVE_PIXMAP = 12298 	# /usr/include/EGL/egl.h:75
+EGL_BAD_NATIVE_WINDOW = 12299 	# /usr/include/EGL/egl.h:76
+EGL_BAD_PARAMETER = 12300 	# /usr/include/EGL/egl.h:77
+EGL_BAD_SURFACE = 12301 	# /usr/include/EGL/egl.h:78
+EGL_BLUE_SIZE = 12322 	# /usr/include/EGL/egl.h:79
+EGL_BUFFER_SIZE = 12320 	# /usr/include/EGL/egl.h:80
+EGL_CONFIG_CAVEAT = 12327 	# /usr/include/EGL/egl.h:81
+EGL_CONFIG_ID = 12328 	# /usr/include/EGL/egl.h:82
+EGL_CORE_NATIVE_ENGINE = 12379 	# /usr/include/EGL/egl.h:83
+EGL_DEPTH_SIZE = 12325 	# /usr/include/EGL/egl.h:84
+EGL_DRAW = 12377 	# /usr/include/EGL/egl.h:86
+EGL_EXTENSIONS = 12373 	# /usr/include/EGL/egl.h:87
+EGL_FALSE = 0 	# /usr/include/EGL/egl.h:88
+EGL_GREEN_SIZE = 12323 	# /usr/include/EGL/egl.h:89
+EGL_HEIGHT = 12374 	# /usr/include/EGL/egl.h:90
+EGL_LARGEST_PBUFFER = 12376 	# /usr/include/EGL/egl.h:91
+EGL_LEVEL = 12329 	# /usr/include/EGL/egl.h:92
+EGL_MAX_PBUFFER_HEIGHT = 12330 	# /usr/include/EGL/egl.h:93
+EGL_MAX_PBUFFER_PIXELS = 12331 	# /usr/include/EGL/egl.h:94
+EGL_MAX_PBUFFER_WIDTH = 12332 	# /usr/include/EGL/egl.h:95
+EGL_NATIVE_RENDERABLE = 12333 	# /usr/include/EGL/egl.h:96
+EGL_NATIVE_VISUAL_ID = 12334 	# /usr/include/EGL/egl.h:97
+EGL_NATIVE_VISUAL_TYPE = 12335 	# /usr/include/EGL/egl.h:98
+EGL_NONE = 12344 	# /usr/include/EGL/egl.h:99
+EGL_NON_CONFORMANT_CONFIG = 12369 	# /usr/include/EGL/egl.h:100
+EGL_NOT_INITIALIZED = 12289 	# /usr/include/EGL/egl.h:101
+EGL_PBUFFER_BIT = 1 	# /usr/include/EGL/egl.h:105
+EGL_PIXMAP_BIT = 2 	# /usr/include/EGL/egl.h:106
+EGL_READ = 12378 	# /usr/include/EGL/egl.h:107
+EGL_RED_SIZE = 12324 	# /usr/include/EGL/egl.h:108
+EGL_SAMPLES = 12337 	# /usr/include/EGL/egl.h:109
+EGL_SAMPLE_BUFFERS = 12338 	# /usr/include/EGL/egl.h:110
+EGL_SLOW_CONFIG = 12368 	# /usr/include/EGL/egl.h:111
+EGL_STENCIL_SIZE = 12326 	# /usr/include/EGL/egl.h:112
+EGL_SUCCESS = 12288 	# /usr/include/EGL/egl.h:113
+EGL_SURFACE_TYPE = 12339 	# /usr/include/EGL/egl.h:114
+EGL_TRANSPARENT_BLUE_VALUE = 12341 	# /usr/include/EGL/egl.h:115
+EGL_TRANSPARENT_GREEN_VALUE = 12342 	# /usr/include/EGL/egl.h:116
+EGL_TRANSPARENT_RED_VALUE = 12343 	# /usr/include/EGL/egl.h:117
+EGL_TRANSPARENT_RGB = 12370 	# /usr/include/EGL/egl.h:118
+EGL_TRANSPARENT_TYPE = 12340 	# /usr/include/EGL/egl.h:119
+EGL_TRUE = 1 	# /usr/include/EGL/egl.h:120
+EGL_VENDOR = 12371 	# /usr/include/EGL/egl.h:121
+EGL_VERSION = 12372 	# /usr/include/EGL/egl.h:122
+EGL_WIDTH = 12375 	# /usr/include/EGL/egl.h:123
+EGL_WINDOW_BIT = 4 	# /usr/include/EGL/egl.h:124
+khronos_int32_t = c_int32 	# /usr/include/KHR/khrplatform.h:150
+EGLint = khronos_int32_t 	# /usr/include/EGL/eglplatform.h:166
+PFNEGLCHOOSECONFIGPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, POINTER(EGLint), POINTER(EGLConfig), EGLint, POINTER(EGLint)) 	# /usr/include/EGL/egl.h:125
+XID = c_ulong 	# /usr/include/X11/X.h:66
+Pixmap = XID 	# /usr/include/X11/X.h:102
+EGLNativePixmapType = Pixmap 	# /usr/include/EGL/eglplatform.h:132
+PFNEGLCOPYBUFFERSPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, EGLSurface, EGLNativePixmapType) 	# /usr/include/EGL/egl.h:126
+PFNEGLCREATECONTEXTPROC = CFUNCTYPE(EGLContext, EGLDisplay, EGLConfig, EGLContext, POINTER(EGLint)) 	# /usr/include/EGL/egl.h:127
+PFNEGLCREATEPBUFFERSURFACEPROC = CFUNCTYPE(EGLSurface, EGLDisplay, EGLConfig, POINTER(EGLint)) 	# /usr/include/EGL/egl.h:128
+PFNEGLCREATEPIXMAPSURFACEPROC = CFUNCTYPE(EGLSurface, EGLDisplay, EGLConfig, EGLNativePixmapType, POINTER(EGLint)) 	# /usr/include/EGL/egl.h:129
+Window = XID 	# /usr/include/X11/X.h:96
+EGLNativeWindowType = Window 	# /usr/include/EGL/eglplatform.h:133
+PFNEGLCREATEWINDOWSURFACEPROC = CFUNCTYPE(EGLSurface, EGLDisplay, EGLConfig, EGLNativeWindowType, POINTER(EGLint)) 	# /usr/include/EGL/egl.h:130
+PFNEGLDESTROYCONTEXTPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, EGLContext) 	# /usr/include/EGL/egl.h:131
+PFNEGLDESTROYSURFACEPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, EGLSurface) 	# /usr/include/EGL/egl.h:132
+PFNEGLGETCONFIGATTRIBPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, EGLConfig, EGLint, POINTER(EGLint)) 	# /usr/include/EGL/egl.h:133
+PFNEGLGETCONFIGSPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, POINTER(EGLConfig), EGLint, POINTER(EGLint)) 	# /usr/include/EGL/egl.h:134
+PFNEGLGETCURRENTDISPLAYPROC = CFUNCTYPE(EGLDisplay) 	# /usr/include/EGL/egl.h:135
+PFNEGLGETCURRENTSURFACEPROC = CFUNCTYPE(EGLSurface, EGLint) 	# /usr/include/EGL/egl.h:136
+class struct__XDisplay(Structure):
+    __slots__ = [
+    ]
+struct__XDisplay._fields_ = [
+    ('_opaque_struct', c_int)
+]
+
+class struct__XDisplay(Structure):
+    __slots__ = [
+    ]
+struct__XDisplay._fields_ = [
+    ('_opaque_struct', c_int)
+]
+
+Display = struct__XDisplay 	# /usr/include/X11/Xlib.h:487
+EGLNativeDisplayType = POINTER(Display) 	# /usr/include/EGL/eglplatform.h:131
+PFNEGLGETDISPLAYPROC = CFUNCTYPE(EGLDisplay, EGLNativeDisplayType) 	# /usr/include/EGL/egl.h:137
+PFNEGLGETERRORPROC = CFUNCTYPE(EGLint) 	# /usr/include/EGL/egl.h:138
+PFNEGLGETPROCADDRESSPROC = CFUNCTYPE(__eglMustCastToProperFunctionPointerType, c_char_p) 	# /usr/include/EGL/egl.h:139
+PFNEGLINITIALIZEPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, POINTER(EGLint), POINTER(EGLint)) 	# /usr/include/EGL/egl.h:140
+PFNEGLMAKECURRENTPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, EGLSurface, EGLSurface, EGLContext) 	# /usr/include/EGL/egl.h:141
+PFNEGLQUERYCONTEXTPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, EGLContext, EGLint, POINTER(EGLint)) 	# /usr/include/EGL/egl.h:142
+PFNEGLQUERYSTRINGPROC = CFUNCTYPE(c_char_p, EGLDisplay, EGLint) 	# /usr/include/EGL/egl.h:143
+PFNEGLQUERYSURFACEPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, EGLSurface, EGLint, POINTER(EGLint)) 	# /usr/include/EGL/egl.h:144
+PFNEGLSWAPBUFFERSPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, EGLSurface) 	# /usr/include/EGL/egl.h:145
+PFNEGLTERMINATEPROC = CFUNCTYPE(EGLBoolean, EGLDisplay) 	# /usr/include/EGL/egl.h:146
+PFNEGLWAITGLPROC = CFUNCTYPE(EGLBoolean) 	# /usr/include/EGL/egl.h:147
+PFNEGLWAITNATIVEPROC = CFUNCTYPE(EGLBoolean, EGLint) 	# /usr/include/EGL/egl.h:148
+# /usr/include/EGL/egl.h:150
+eglChooseConfig = _lib.eglChooseConfig
+eglChooseConfig.restype = EGLBoolean
+eglChooseConfig.argtypes = [EGLDisplay, POINTER(EGLint), POINTER(EGLConfig), EGLint, POINTER(EGLint)]
+
+# /usr/include/EGL/egl.h:151
+eglCopyBuffers = _lib.eglCopyBuffers
+eglCopyBuffers.restype = EGLBoolean
+eglCopyBuffers.argtypes = [EGLDisplay, EGLSurface, EGLNativePixmapType]
+
+# /usr/include/EGL/egl.h:152
+eglCreateContext = _lib.eglCreateContext
+eglCreateContext.restype = EGLContext
+eglCreateContext.argtypes = [EGLDisplay, EGLConfig, EGLContext, POINTER(EGLint)]
+
+# /usr/include/EGL/egl.h:153
+eglCreatePbufferSurface = _lib.eglCreatePbufferSurface
+eglCreatePbufferSurface.restype = EGLSurface
+eglCreatePbufferSurface.argtypes = [EGLDisplay, EGLConfig, POINTER(EGLint)]
+
+# /usr/include/EGL/egl.h:154
+eglCreatePixmapSurface = _lib.eglCreatePixmapSurface
+eglCreatePixmapSurface.restype = EGLSurface
+eglCreatePixmapSurface.argtypes = [EGLDisplay, EGLConfig, EGLNativePixmapType, POINTER(EGLint)]
+
+# /usr/include/EGL/egl.h:155
+eglCreateWindowSurface = _lib.eglCreateWindowSurface
+eglCreateWindowSurface.restype = EGLSurface
+eglCreateWindowSurface.argtypes = [EGLDisplay, EGLConfig, EGLNativeWindowType, POINTER(EGLint)]
+
+# /usr/include/EGL/egl.h:156
+eglDestroyContext = _lib.eglDestroyContext
+eglDestroyContext.restype = EGLBoolean
+eglDestroyContext.argtypes = [EGLDisplay, EGLContext]
+
+# /usr/include/EGL/egl.h:157
+eglDestroySurface = _lib.eglDestroySurface
+eglDestroySurface.restype = EGLBoolean
+eglDestroySurface.argtypes = [EGLDisplay, EGLSurface]
+
+# /usr/include/EGL/egl.h:158
+eglGetConfigAttrib = _lib.eglGetConfigAttrib
+eglGetConfigAttrib.restype = EGLBoolean
+eglGetConfigAttrib.argtypes = [EGLDisplay, EGLConfig, EGLint, POINTER(EGLint)]
+
+# /usr/include/EGL/egl.h:159
+eglGetConfigs = _lib.eglGetConfigs
+eglGetConfigs.restype = EGLBoolean
+eglGetConfigs.argtypes = [EGLDisplay, POINTER(EGLConfig), EGLint, POINTER(EGLint)]
+
+# /usr/include/EGL/egl.h:160
+eglGetCurrentDisplay = _lib.eglGetCurrentDisplay
+eglGetCurrentDisplay.restype = EGLDisplay
+eglGetCurrentDisplay.argtypes = []
+
+# /usr/include/EGL/egl.h:161
+eglGetCurrentSurface = _lib.eglGetCurrentSurface
+eglGetCurrentSurface.restype = EGLSurface
+eglGetCurrentSurface.argtypes = [EGLint]
+
+# /usr/include/EGL/egl.h:162
+eglGetDisplay = _lib.eglGetDisplay
+eglGetDisplay.restype = EGLDisplay
+eglGetDisplay.argtypes = [EGLNativeDisplayType]
+
+# /usr/include/EGL/egl.h:163
+eglGetError = _lib.eglGetError
+eglGetError.restype = EGLint
+eglGetError.argtypes = []
+
+# /usr/include/EGL/egl.h:164
+eglGetProcAddress = _lib.eglGetProcAddress
+eglGetProcAddress.restype = __eglMustCastToProperFunctionPointerType
+eglGetProcAddress.argtypes = [c_char_p]
+
+# /usr/include/EGL/egl.h:165
+eglInitialize = _lib.eglInitialize
+eglInitialize.restype = EGLBoolean
+eglInitialize.argtypes = [EGLDisplay, POINTER(EGLint), POINTER(EGLint)]
+
+# /usr/include/EGL/egl.h:166
+eglMakeCurrent = _lib.eglMakeCurrent
+eglMakeCurrent.restype = EGLBoolean
+eglMakeCurrent.argtypes = [EGLDisplay, EGLSurface, EGLSurface, EGLContext]
+
+# /usr/include/EGL/egl.h:167
+eglQueryContext = _lib.eglQueryContext
+eglQueryContext.restype = EGLBoolean
+eglQueryContext.argtypes = [EGLDisplay, EGLContext, EGLint, POINTER(EGLint)]
+
+# /usr/include/EGL/egl.h:168
+eglQueryString = _lib.eglQueryString
+eglQueryString.restype = c_char_p
+eglQueryString.argtypes = [EGLDisplay, EGLint]
+
+# /usr/include/EGL/egl.h:169
+eglQuerySurface = _lib.eglQuerySurface
+eglQuerySurface.restype = EGLBoolean
+eglQuerySurface.argtypes = [EGLDisplay, EGLSurface, EGLint, POINTER(EGLint)]
+
+# /usr/include/EGL/egl.h:170
+eglSwapBuffers = _lib.eglSwapBuffers
+eglSwapBuffers.restype = EGLBoolean
+eglSwapBuffers.argtypes = [EGLDisplay, EGLSurface]
+
+# /usr/include/EGL/egl.h:171
+eglTerminate = _lib.eglTerminate
+eglTerminate.restype = EGLBoolean
+eglTerminate.argtypes = [EGLDisplay]
+
+# /usr/include/EGL/egl.h:172
+eglWaitGL = _lib.eglWaitGL
+eglWaitGL.restype = EGLBoolean
+eglWaitGL.argtypes = []
+
+# /usr/include/EGL/egl.h:173
+eglWaitNative = _lib.eglWaitNative
+eglWaitNative.restype = EGLBoolean
+eglWaitNative.argtypes = [EGLint]
+
+EGL_VERSION_1_1 = 1 	# /usr/include/EGL/egl.h:178
+EGL_BACK_BUFFER = 12420 	# /usr/include/EGL/egl.h:179
+EGL_BIND_TO_TEXTURE_RGB = 12345 	# /usr/include/EGL/egl.h:180
+EGL_BIND_TO_TEXTURE_RGBA = 12346 	# /usr/include/EGL/egl.h:181
+EGL_CONTEXT_LOST = 12302 	# /usr/include/EGL/egl.h:182
+EGL_MIN_SWAP_INTERVAL = 12347 	# /usr/include/EGL/egl.h:183
+EGL_MAX_SWAP_INTERVAL = 12348 	# /usr/include/EGL/egl.h:184
+EGL_MIPMAP_TEXTURE = 12418 	# /usr/include/EGL/egl.h:185
+EGL_MIPMAP_LEVEL = 12419 	# /usr/include/EGL/egl.h:186
+EGL_NO_TEXTURE = 12380 	# /usr/include/EGL/egl.h:187
+EGL_TEXTURE_2D = 12383 	# /usr/include/EGL/egl.h:188
+EGL_TEXTURE_FORMAT = 12416 	# /usr/include/EGL/egl.h:189
+EGL_TEXTURE_RGB = 12381 	# /usr/include/EGL/egl.h:190
+EGL_TEXTURE_RGBA = 12382 	# /usr/include/EGL/egl.h:191
+EGL_TEXTURE_TARGET = 12417 	# /usr/include/EGL/egl.h:192
+PFNEGLBINDTEXIMAGEPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, EGLSurface, EGLint) 	# /usr/include/EGL/egl.h:193
+PFNEGLRELEASETEXIMAGEPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, EGLSurface, EGLint) 	# /usr/include/EGL/egl.h:194
+PFNEGLSURFACEATTRIBPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, EGLSurface, EGLint, EGLint) 	# /usr/include/EGL/egl.h:195
+PFNEGLSWAPINTERVALPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, EGLint) 	# /usr/include/EGL/egl.h:196
+# /usr/include/EGL/egl.h:198
+eglBindTexImage = _lib.eglBindTexImage
+eglBindTexImage.restype = EGLBoolean
+eglBindTexImage.argtypes = [EGLDisplay, EGLSurface, EGLint]
+
+# /usr/include/EGL/egl.h:199
+eglReleaseTexImage = _lib.eglReleaseTexImage
+eglReleaseTexImage.restype = EGLBoolean
+eglReleaseTexImage.argtypes = [EGLDisplay, EGLSurface, EGLint]
+
+# /usr/include/EGL/egl.h:200
+eglSurfaceAttrib = _lib.eglSurfaceAttrib
+eglSurfaceAttrib.restype = EGLBoolean
+eglSurfaceAttrib.argtypes = [EGLDisplay, EGLSurface, EGLint, EGLint]
+
+# /usr/include/EGL/egl.h:201
+eglSwapInterval = _lib.eglSwapInterval
+eglSwapInterval.restype = EGLBoolean
+eglSwapInterval.argtypes = [EGLDisplay, EGLint]
+
+EGL_VERSION_1_2 = 1 	# /usr/include/EGL/egl.h:206
+EGLenum = c_uint 	# /usr/include/EGL/egl.h:207
+EGLClientBuffer = POINTER(None) 	# /usr/include/EGL/egl.h:208
+EGL_ALPHA_FORMAT = 12424 	# /usr/include/EGL/egl.h:209
+EGL_ALPHA_FORMAT_NONPRE = 12427 	# /usr/include/EGL/egl.h:210
+EGL_ALPHA_FORMAT_PRE = 12428 	# /usr/include/EGL/egl.h:211
+EGL_ALPHA_MASK_SIZE = 12350 	# /usr/include/EGL/egl.h:212
+EGL_BUFFER_PRESERVED = 12436 	# /usr/include/EGL/egl.h:213
+EGL_BUFFER_DESTROYED = 12437 	# /usr/include/EGL/egl.h:214
+EGL_CLIENT_APIS = 12429 	# /usr/include/EGL/egl.h:215
+EGL_COLORSPACE = 12423 	# /usr/include/EGL/egl.h:216
+EGL_COLORSPACE_sRGB = 12425 	# /usr/include/EGL/egl.h:217
+EGL_COLORSPACE_LINEAR = 12426 	# /usr/include/EGL/egl.h:218
+EGL_COLOR_BUFFER_TYPE = 12351 	# /usr/include/EGL/egl.h:219
+EGL_CONTEXT_CLIENT_TYPE = 12439 	# /usr/include/EGL/egl.h:220
+EGL_DISPLAY_SCALING = 10000 	# /usr/include/EGL/egl.h:221
+EGL_HORIZONTAL_RESOLUTION = 12432 	# /usr/include/EGL/egl.h:222
+EGL_LUMINANCE_BUFFER = 12431 	# /usr/include/EGL/egl.h:223
+EGL_LUMINANCE_SIZE = 12349 	# /usr/include/EGL/egl.h:224
+EGL_OPENGL_ES_BIT = 1 	# /usr/include/EGL/egl.h:225
+EGL_OPENVG_BIT = 2 	# /usr/include/EGL/egl.h:226
+EGL_OPENGL_ES_API = 12448 	# /usr/include/EGL/egl.h:227
+EGL_OPENVG_API = 12449 	# /usr/include/EGL/egl.h:228
+EGL_OPENVG_IMAGE = 12438 	# /usr/include/EGL/egl.h:229
+EGL_PIXEL_ASPECT_RATIO = 12434 	# /usr/include/EGL/egl.h:230
+EGL_RENDERABLE_TYPE = 12352 	# /usr/include/EGL/egl.h:231
+EGL_RENDER_BUFFER = 12422 	# /usr/include/EGL/egl.h:232
+EGL_RGB_BUFFER = 12430 	# /usr/include/EGL/egl.h:233
+EGL_SINGLE_BUFFER = 12421 	# /usr/include/EGL/egl.h:234
+EGL_SWAP_BEHAVIOR = 12435 	# /usr/include/EGL/egl.h:235
+EGL_VERTICAL_RESOLUTION = 12433 	# /usr/include/EGL/egl.h:237
+PFNEGLBINDAPIPROC = CFUNCTYPE(EGLBoolean, EGLenum) 	# /usr/include/EGL/egl.h:238
+PFNEGLQUERYAPIPROC = CFUNCTYPE(EGLenum) 	# /usr/include/EGL/egl.h:239
+PFNEGLCREATEPBUFFERFROMCLIENTBUFFERPROC = CFUNCTYPE(EGLSurface, EGLDisplay, EGLenum, EGLClientBuffer, EGLConfig, POINTER(EGLint)) 	# /usr/include/EGL/egl.h:240
+PFNEGLRELEASETHREADPROC = CFUNCTYPE(EGLBoolean) 	# /usr/include/EGL/egl.h:241
+PFNEGLWAITCLIENTPROC = CFUNCTYPE(EGLBoolean) 	# /usr/include/EGL/egl.h:242
+# /usr/include/EGL/egl.h:244
+eglBindAPI = _lib.eglBindAPI
+eglBindAPI.restype = EGLBoolean
+eglBindAPI.argtypes = [EGLenum]
+
+# /usr/include/EGL/egl.h:245
+eglQueryAPI = _lib.eglQueryAPI
+eglQueryAPI.restype = EGLenum
+eglQueryAPI.argtypes = []
+
+# /usr/include/EGL/egl.h:246
+eglCreatePbufferFromClientBuffer = _lib.eglCreatePbufferFromClientBuffer
+eglCreatePbufferFromClientBuffer.restype = EGLSurface
+eglCreatePbufferFromClientBuffer.argtypes = [EGLDisplay, EGLenum, EGLClientBuffer, EGLConfig, POINTER(EGLint)]
+
+# /usr/include/EGL/egl.h:247
+eglReleaseThread = _lib.eglReleaseThread
+eglReleaseThread.restype = EGLBoolean
+eglReleaseThread.argtypes = []
+
+# /usr/include/EGL/egl.h:248
+eglWaitClient = _lib.eglWaitClient
+eglWaitClient.restype = EGLBoolean
+eglWaitClient.argtypes = []
+
+EGL_VERSION_1_3 = 1 	# /usr/include/EGL/egl.h:253
+EGL_CONFORMANT = 12354 	# /usr/include/EGL/egl.h:254
+EGL_CONTEXT_CLIENT_VERSION = 12440 	# /usr/include/EGL/egl.h:255
+EGL_MATCH_NATIVE_PIXMAP = 12353 	# /usr/include/EGL/egl.h:256
+EGL_OPENGL_ES2_BIT = 4 	# /usr/include/EGL/egl.h:257
+EGL_VG_ALPHA_FORMAT = 12424 	# /usr/include/EGL/egl.h:258
+EGL_VG_ALPHA_FORMAT_NONPRE = 12427 	# /usr/include/EGL/egl.h:259
+EGL_VG_ALPHA_FORMAT_PRE = 12428 	# /usr/include/EGL/egl.h:260
+EGL_VG_ALPHA_FORMAT_PRE_BIT = 64 	# /usr/include/EGL/egl.h:261
+EGL_VG_COLORSPACE = 12423 	# /usr/include/EGL/egl.h:262
+EGL_VG_COLORSPACE_sRGB = 12425 	# /usr/include/EGL/egl.h:263
+EGL_VG_COLORSPACE_LINEAR = 12426 	# /usr/include/EGL/egl.h:264
+EGL_VG_COLORSPACE_LINEAR_BIT = 32 	# /usr/include/EGL/egl.h:265
+EGL_VERSION_1_4 = 1 	# /usr/include/EGL/egl.h:269
+EGL_MULTISAMPLE_RESOLVE_BOX_BIT = 512 	# /usr/include/EGL/egl.h:271
+EGL_MULTISAMPLE_RESOLVE = 12441 	# /usr/include/EGL/egl.h:272
+EGL_MULTISAMPLE_RESOLVE_DEFAULT = 12442 	# /usr/include/EGL/egl.h:273
+EGL_MULTISAMPLE_RESOLVE_BOX = 12443 	# /usr/include/EGL/egl.h:274
+EGL_OPENGL_API = 12450 	# /usr/include/EGL/egl.h:275
+EGL_OPENGL_BIT = 8 	# /usr/include/EGL/egl.h:276
+EGL_SWAP_BEHAVIOR_PRESERVED_BIT = 1024 	# /usr/include/EGL/egl.h:277
+PFNEGLGETCURRENTCONTEXTPROC = CFUNCTYPE(EGLContext) 	# /usr/include/EGL/egl.h:278
+# /usr/include/EGL/egl.h:280
+eglGetCurrentContext = _lib.eglGetCurrentContext
+eglGetCurrentContext.restype = EGLContext
+eglGetCurrentContext.argtypes = []
+
+EGL_VERSION_1_5 = 1 	# /usr/include/EGL/egl.h:285
+EGLSync = POINTER(None) 	# /usr/include/EGL/egl.h:286
+intptr_t = c_long 	# /usr/include/stdint.h:87
+EGLAttrib = intptr_t 	# /usr/include/EGL/egl.h:287
+khronos_uint64_t = c_uint64 	# /usr/include/KHR/khrplatform.h:153
+khronos_utime_nanoseconds_t = khronos_uint64_t 	# /usr/include/KHR/khrplatform.h:267
+EGLTime = khronos_utime_nanoseconds_t 	# /usr/include/EGL/egl.h:288
+EGLImage = POINTER(None) 	# /usr/include/EGL/egl.h:289
+EGL_CONTEXT_MAJOR_VERSION = 12440 	# /usr/include/EGL/egl.h:290
+EGL_CONTEXT_MINOR_VERSION = 12539 	# /usr/include/EGL/egl.h:291
+EGL_CONTEXT_OPENGL_PROFILE_MASK = 12541 	# /usr/include/EGL/egl.h:292
+EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY = 12733 	# /usr/include/EGL/egl.h:293
+EGL_NO_RESET_NOTIFICATION = 12734 	# /usr/include/EGL/egl.h:294
+EGL_LOSE_CONTEXT_ON_RESET = 12735 	# /usr/include/EGL/egl.h:295
+EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT = 1 	# /usr/include/EGL/egl.h:296
+EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT = 2 	# /usr/include/EGL/egl.h:297
+EGL_CONTEXT_OPENGL_DEBUG = 12720 	# /usr/include/EGL/egl.h:298
+EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE = 12721 	# /usr/include/EGL/egl.h:299
+EGL_CONTEXT_OPENGL_ROBUST_ACCESS = 12722 	# /usr/include/EGL/egl.h:300
+EGL_OPENGL_ES3_BIT = 64 	# /usr/include/EGL/egl.h:301
+EGL_CL_EVENT_HANDLE = 12444 	# /usr/include/EGL/egl.h:302
+EGL_SYNC_CL_EVENT = 12542 	# /usr/include/EGL/egl.h:303
+EGL_SYNC_CL_EVENT_COMPLETE = 12543 	# /usr/include/EGL/egl.h:304
+EGL_SYNC_PRIOR_COMMANDS_COMPLETE = 12528 	# /usr/include/EGL/egl.h:305
+EGL_SYNC_TYPE = 12535 	# /usr/include/EGL/egl.h:306
+EGL_SYNC_STATUS = 12529 	# /usr/include/EGL/egl.h:307
+EGL_SYNC_CONDITION = 12536 	# /usr/include/EGL/egl.h:308
+EGL_SIGNALED = 12530 	# /usr/include/EGL/egl.h:309
+EGL_UNSIGNALED = 12531 	# /usr/include/EGL/egl.h:310
+EGL_SYNC_FLUSH_COMMANDS_BIT = 1 	# /usr/include/EGL/egl.h:311
+EGL_FOREVER = 18446744073709551615 	# /usr/include/EGL/egl.h:312
+EGL_TIMEOUT_EXPIRED = 12533 	# /usr/include/EGL/egl.h:313
+EGL_CONDITION_SATISFIED = 12534 	# /usr/include/EGL/egl.h:314
+EGL_SYNC_FENCE = 12537 	# /usr/include/EGL/egl.h:316
+EGL_GL_COLORSPACE = 12445 	# /usr/include/EGL/egl.h:317
+EGL_GL_COLORSPACE_SRGB = 12425 	# /usr/include/EGL/egl.h:318
+EGL_GL_COLORSPACE_LINEAR = 12426 	# /usr/include/EGL/egl.h:319
+EGL_GL_RENDERBUFFER = 12473 	# /usr/include/EGL/egl.h:320
+EGL_GL_TEXTURE_2D = 12465 	# /usr/include/EGL/egl.h:321
+EGL_GL_TEXTURE_LEVEL = 12476 	# /usr/include/EGL/egl.h:322
+EGL_GL_TEXTURE_3D = 12466 	# /usr/include/EGL/egl.h:323
+EGL_GL_TEXTURE_ZOFFSET = 12477 	# /usr/include/EGL/egl.h:324
+EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_X = 12467 	# /usr/include/EGL/egl.h:325
+EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_X = 12468 	# /usr/include/EGL/egl.h:326
+EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_Y = 12469 	# /usr/include/EGL/egl.h:327
+EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Y = 12470 	# /usr/include/EGL/egl.h:328
+EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_Z = 12471 	# /usr/include/EGL/egl.h:329
+EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Z = 12472 	# /usr/include/EGL/egl.h:330
+EGL_IMAGE_PRESERVED = 12498 	# /usr/include/EGL/egl.h:331
+PFNEGLCREATESYNCPROC = CFUNCTYPE(EGLSync, EGLDisplay, EGLenum, POINTER(EGLAttrib)) 	# /usr/include/EGL/egl.h:333
+PFNEGLDESTROYSYNCPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, EGLSync) 	# /usr/include/EGL/egl.h:334
+PFNEGLCLIENTWAITSYNCPROC = CFUNCTYPE(EGLint, EGLDisplay, EGLSync, EGLint, EGLTime) 	# /usr/include/EGL/egl.h:335
+PFNEGLGETSYNCATTRIBPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, EGLSync, EGLint, POINTER(EGLAttrib)) 	# /usr/include/EGL/egl.h:336
+PFNEGLCREATEIMAGEPROC = CFUNCTYPE(EGLImage, EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, POINTER(EGLAttrib)) 	# /usr/include/EGL/egl.h:337
+PFNEGLDESTROYIMAGEPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, EGLImage) 	# /usr/include/EGL/egl.h:338
+PFNEGLGETPLATFORMDISPLAYPROC = CFUNCTYPE(EGLDisplay, EGLenum, POINTER(None), POINTER(EGLAttrib)) 	# /usr/include/EGL/egl.h:339
+PFNEGLCREATEPLATFORMWINDOWSURFACEPROC = CFUNCTYPE(EGLSurface, EGLDisplay, EGLConfig, POINTER(None), POINTER(EGLAttrib)) 	# /usr/include/EGL/egl.h:340
+PFNEGLCREATEPLATFORMPIXMAPSURFACEPROC = CFUNCTYPE(EGLSurface, EGLDisplay, EGLConfig, POINTER(None), POINTER(EGLAttrib)) 	# /usr/include/EGL/egl.h:341
+PFNEGLWAITSYNCPROC = CFUNCTYPE(EGLBoolean, EGLDisplay, EGLSync, EGLint) 	# /usr/include/EGL/egl.h:342
+# /usr/include/EGL/egl.h:344
+eglCreateSync = _lib.eglCreateSync
+eglCreateSync.restype = EGLSync
+eglCreateSync.argtypes = [EGLDisplay, EGLenum, POINTER(EGLAttrib)]
+
+# /usr/include/EGL/egl.h:345
+eglDestroySync = _lib.eglDestroySync
+eglDestroySync.restype = EGLBoolean
+eglDestroySync.argtypes = [EGLDisplay, EGLSync]
+
+# /usr/include/EGL/egl.h:346
+eglClientWaitSync = _lib.eglClientWaitSync
+eglClientWaitSync.restype = EGLint
+eglClientWaitSync.argtypes = [EGLDisplay, EGLSync, EGLint, EGLTime]
+
+# /usr/include/EGL/egl.h:347
+eglGetSyncAttrib = _lib.eglGetSyncAttrib
+eglGetSyncAttrib.restype = EGLBoolean
+eglGetSyncAttrib.argtypes = [EGLDisplay, EGLSync, EGLint, POINTER(EGLAttrib)]
+
+# /usr/include/EGL/egl.h:348
+eglCreateImage = _lib.eglCreateImage
+eglCreateImage.restype = EGLImage
+eglCreateImage.argtypes = [EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, POINTER(EGLAttrib)]
+
+# /usr/include/EGL/egl.h:349
+eglDestroyImage = _lib.eglDestroyImage
+eglDestroyImage.restype = EGLBoolean
+eglDestroyImage.argtypes = [EGLDisplay, EGLImage]
+
+# /usr/include/EGL/egl.h:350
+eglGetPlatformDisplay = _lib.eglGetPlatformDisplay
+eglGetPlatformDisplay.restype = EGLDisplay
+eglGetPlatformDisplay.argtypes = [EGLenum, POINTER(None), POINTER(EGLAttrib)]
+
+# /usr/include/EGL/egl.h:351
+eglCreatePlatformWindowSurface = _lib.eglCreatePlatformWindowSurface
+eglCreatePlatformWindowSurface.restype = EGLSurface
+eglCreatePlatformWindowSurface.argtypes = [EGLDisplay, EGLConfig, POINTER(None), POINTER(EGLAttrib)]
+
+# /usr/include/EGL/egl.h:352
+eglCreatePlatformPixmapSurface = _lib.eglCreatePlatformPixmapSurface
+eglCreatePlatformPixmapSurface.restype = EGLSurface
+eglCreatePlatformPixmapSurface.argtypes = [EGLDisplay, EGLConfig, POINTER(None), POINTER(EGLAttrib)]
+
+# /usr/include/EGL/egl.h:353
+eglWaitSync = _lib.eglWaitSync
+eglWaitSync.restype = EGLBoolean
+eglWaitSync.argtypes = [EGLDisplay, EGLSync, EGLint]
+
+
+__all__ = ['__egl_h_', 'EGL_EGL_PROTOTYPES', 'EGL_VERSION_1_0', 'EGLBoolean',
+'EGLDisplay', 'EGLConfig', 'EGLSurface', 'EGLContext',
+'__eglMustCastToProperFunctionPointerType', 'EGL_ALPHA_SIZE',
+'EGL_BAD_ACCESS', 'EGL_BAD_ALLOC', 'EGL_BAD_ATTRIBUTE', 'EGL_BAD_CONFIG',
+'EGL_BAD_CONTEXT', 'EGL_BAD_CURRENT_SURFACE', 'EGL_BAD_DISPLAY',
+'EGL_BAD_MATCH', 'EGL_BAD_NATIVE_PIXMAP', 'EGL_BAD_NATIVE_WINDOW',
+'EGL_BAD_PARAMETER', 'EGL_BAD_SURFACE', 'EGL_BLUE_SIZE', 'EGL_BUFFER_SIZE',
+'EGL_CONFIG_CAVEAT', 'EGL_CONFIG_ID', 'EGL_CORE_NATIVE_ENGINE',
+'EGL_DEPTH_SIZE', 'EGL_DRAW', 'EGL_EXTENSIONS', 'EGL_FALSE', 'EGL_GREEN_SIZE',
+'EGL_HEIGHT', 'EGL_LARGEST_PBUFFER', 'EGL_LEVEL', 'EGL_MAX_PBUFFER_HEIGHT',
+'EGL_MAX_PBUFFER_PIXELS', 'EGL_MAX_PBUFFER_WIDTH', 'EGL_NATIVE_RENDERABLE',
+'EGL_NATIVE_VISUAL_ID', 'EGL_NATIVE_VISUAL_TYPE', 'EGL_NONE',
+'EGL_NON_CONFORMANT_CONFIG', 'EGL_NOT_INITIALIZED', 'EGL_PBUFFER_BIT',
+'EGL_PIXMAP_BIT', 'EGL_READ', 'EGL_RED_SIZE', 'EGL_SAMPLES',
+'EGL_SAMPLE_BUFFERS', 'EGL_SLOW_CONFIG', 'EGL_STENCIL_SIZE', 'EGL_SUCCESS',
+'EGL_SURFACE_TYPE', 'EGL_TRANSPARENT_BLUE_VALUE',
+'EGL_TRANSPARENT_GREEN_VALUE', 'EGL_TRANSPARENT_RED_VALUE',
+'EGL_TRANSPARENT_RGB', 'EGL_TRANSPARENT_TYPE', 'EGL_TRUE', 'EGL_VENDOR',
+'EGL_VERSION', 'EGL_WIDTH', 'EGL_WINDOW_BIT', 'PFNEGLCHOOSECONFIGPROC',
+'PFNEGLCOPYBUFFERSPROC', 'PFNEGLCREATECONTEXTPROC',
+'PFNEGLCREATEPBUFFERSURFACEPROC', 'PFNEGLCREATEPIXMAPSURFACEPROC',
+'PFNEGLCREATEWINDOWSURFACEPROC', 'PFNEGLDESTROYCONTEXTPROC',
+'PFNEGLDESTROYSURFACEPROC', 'PFNEGLGETCONFIGATTRIBPROC',
+'PFNEGLGETCONFIGSPROC', 'PFNEGLGETCURRENTDISPLAYPROC',
+'PFNEGLGETCURRENTSURFACEPROC', 'PFNEGLGETDISPLAYPROC', 'PFNEGLGETERRORPROC',
+'PFNEGLGETPROCADDRESSPROC', 'PFNEGLINITIALIZEPROC', 'PFNEGLMAKECURRENTPROC',
+'PFNEGLQUERYCONTEXTPROC', 'PFNEGLQUERYSTRINGPROC', 'PFNEGLQUERYSURFACEPROC',
+'PFNEGLSWAPBUFFERSPROC', 'PFNEGLTERMINATEPROC', 'PFNEGLWAITGLPROC',
+'PFNEGLWAITNATIVEPROC', 'eglChooseConfig', 'eglCopyBuffers',
+'eglCreateContext', 'eglCreatePbufferSurface', 'eglCreatePixmapSurface',
+'eglCreateWindowSurface', 'eglDestroyContext', 'eglDestroySurface',
+'eglGetConfigAttrib', 'eglGetConfigs', 'eglGetCurrentDisplay',
+'eglGetCurrentSurface', 'eglGetDisplay', 'eglGetError', 'eglGetProcAddress',
+'eglInitialize', 'eglMakeCurrent', 'eglQueryContext', 'eglQueryString',
+'eglQuerySurface', 'eglSwapBuffers', 'eglTerminate', 'eglWaitGL',
+'eglWaitNative', 'EGL_VERSION_1_1', 'EGL_BACK_BUFFER',
+'EGL_BIND_TO_TEXTURE_RGB', 'EGL_BIND_TO_TEXTURE_RGBA', 'EGL_CONTEXT_LOST',
+'EGL_MIN_SWAP_INTERVAL', 'EGL_MAX_SWAP_INTERVAL', 'EGL_MIPMAP_TEXTURE',
+'EGL_MIPMAP_LEVEL', 'EGL_NO_TEXTURE', 'EGL_TEXTURE_2D', 'EGL_TEXTURE_FORMAT',
+'EGL_TEXTURE_RGB', 'EGL_TEXTURE_RGBA', 'EGL_TEXTURE_TARGET',
+'PFNEGLBINDTEXIMAGEPROC', 'PFNEGLRELEASETEXIMAGEPROC',
+'PFNEGLSURFACEATTRIBPROC', 'PFNEGLSWAPINTERVALPROC', 'eglBindTexImage',
+'eglReleaseTexImage', 'eglSurfaceAttrib', 'eglSwapInterval',
+'EGL_VERSION_1_2', 'EGLenum', 'EGLClientBuffer', 'EGL_ALPHA_FORMAT',
+'EGL_ALPHA_FORMAT_NONPRE', 'EGL_ALPHA_FORMAT_PRE', 'EGL_ALPHA_MASK_SIZE',
+'EGL_BUFFER_PRESERVED', 'EGL_BUFFER_DESTROYED', 'EGL_CLIENT_APIS',
+'EGL_COLORSPACE', 'EGL_COLORSPACE_sRGB', 'EGL_COLORSPACE_LINEAR',
+'EGL_COLOR_BUFFER_TYPE', 'EGL_CONTEXT_CLIENT_TYPE', 'EGL_DISPLAY_SCALING',
+'EGL_HORIZONTAL_RESOLUTION', 'EGL_LUMINANCE_BUFFER', 'EGL_LUMINANCE_SIZE',
+'EGL_OPENGL_ES_BIT', 'EGL_OPENVG_BIT', 'EGL_OPENGL_ES_API', 'EGL_OPENVG_API',
+'EGL_OPENVG_IMAGE', 'EGL_PIXEL_ASPECT_RATIO', 'EGL_RENDERABLE_TYPE',
+'EGL_RENDER_BUFFER', 'EGL_RGB_BUFFER', 'EGL_SINGLE_BUFFER',
+'EGL_SWAP_BEHAVIOR', 'EGL_VERTICAL_RESOLUTION', 'PFNEGLBINDAPIPROC',
+'PFNEGLQUERYAPIPROC', 'PFNEGLCREATEPBUFFERFROMCLIENTBUFFERPROC',
+'PFNEGLRELEASETHREADPROC', 'PFNEGLWAITCLIENTPROC', 'eglBindAPI',
+'eglQueryAPI', 'eglCreatePbufferFromClientBuffer', 'eglReleaseThread',
+'eglWaitClient', 'EGL_VERSION_1_3', 'EGL_CONFORMANT',
+'EGL_CONTEXT_CLIENT_VERSION', 'EGL_MATCH_NATIVE_PIXMAP', 'EGL_OPENGL_ES2_BIT',
+'EGL_VG_ALPHA_FORMAT', 'EGL_VG_ALPHA_FORMAT_NONPRE',
+'EGL_VG_ALPHA_FORMAT_PRE', 'EGL_VG_ALPHA_FORMAT_PRE_BIT', 'EGL_VG_COLORSPACE',
+'EGL_VG_COLORSPACE_sRGB', 'EGL_VG_COLORSPACE_LINEAR',
+'EGL_VG_COLORSPACE_LINEAR_BIT', 'EGL_VERSION_1_4',
+'EGL_MULTISAMPLE_RESOLVE_BOX_BIT', 'EGL_MULTISAMPLE_RESOLVE',
+'EGL_MULTISAMPLE_RESOLVE_DEFAULT', 'EGL_MULTISAMPLE_RESOLVE_BOX',
+'EGL_OPENGL_API', 'EGL_OPENGL_BIT', 'EGL_SWAP_BEHAVIOR_PRESERVED_BIT',
+'PFNEGLGETCURRENTCONTEXTPROC', 'eglGetCurrentContext', 'EGL_VERSION_1_5',
+'EGLSync', 'EGLAttrib', 'EGLTime', 'EGLImage', 'EGL_CONTEXT_MAJOR_VERSION',
+'EGL_CONTEXT_MINOR_VERSION', 'EGL_CONTEXT_OPENGL_PROFILE_MASK',
+'EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY', 'EGL_NO_RESET_NOTIFICATION',
+'EGL_LOSE_CONTEXT_ON_RESET', 'EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT',
+'EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT', 'EGL_CONTEXT_OPENGL_DEBUG',
+'EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE', 'EGL_CONTEXT_OPENGL_ROBUST_ACCESS',
+'EGL_OPENGL_ES3_BIT', 'EGL_CL_EVENT_HANDLE', 'EGL_SYNC_CL_EVENT',
+'EGL_SYNC_CL_EVENT_COMPLETE', 'EGL_SYNC_PRIOR_COMMANDS_COMPLETE',
+'EGL_SYNC_TYPE', 'EGL_SYNC_STATUS', 'EGL_SYNC_CONDITION', 'EGL_SIGNALED',
+'EGL_UNSIGNALED', 'EGL_SYNC_FLUSH_COMMANDS_BIT', 'EGL_FOREVER',
+'EGL_TIMEOUT_EXPIRED', 'EGL_CONDITION_SATISFIED', 'EGL_SYNC_FENCE',
+'EGL_GL_COLORSPACE', 'EGL_GL_COLORSPACE_SRGB', 'EGL_GL_COLORSPACE_LINEAR',
+'EGL_GL_RENDERBUFFER', 'EGL_GL_TEXTURE_2D', 'EGL_GL_TEXTURE_LEVEL',
+'EGL_GL_TEXTURE_3D', 'EGL_GL_TEXTURE_ZOFFSET',
+'EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_X', 'EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_X',
+'EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_Y', 'EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Y',
+'EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_Z', 'EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Z',
+'EGL_IMAGE_PRESERVED', 'PFNEGLCREATESYNCPROC', 'PFNEGLDESTROYSYNCPROC',
+'PFNEGLCLIENTWAITSYNCPROC', 'PFNEGLGETSYNCATTRIBPROC',
+'PFNEGLCREATEIMAGEPROC', 'PFNEGLDESTROYIMAGEPROC',
+'PFNEGLGETPLATFORMDISPLAYPROC', 'PFNEGLCREATEPLATFORMWINDOWSURFACEPROC',
+'PFNEGLCREATEPLATFORMPIXMAPSURFACEPROC', 'PFNEGLWAITSYNCPROC',
+'eglCreateSync', 'eglDestroySync', 'eglClientWaitSync', 'eglGetSyncAttrib',
+'eglCreateImage', 'eglDestroyImage', 'eglGetPlatformDisplay',
+'eglCreatePlatformWindowSurface', 'eglCreatePlatformPixmapSurface',
+'eglWaitSync']
diff --git a/pyglet/libs/egl/eglext.py b/pyglet/libs/egl/eglext.py
new file mode 100644
index 0000000..fd93bc8
--- /dev/null
+++ b/pyglet/libs/egl/eglext.py
@@ -0,0 +1,10 @@
+from ctypes import *
+from pyglet.libs.egl import egl
+from pyglet.libs.egl.lib import link_EGL as _link_function
+
+EGL_PLATFORM_DEVICE_EXT = 12607
+EGLDeviceEXT = POINTER(None)
+eglGetPlatformDisplayEXT = _link_function('eglGetPlatformDisplayEXT', egl.EGLDisplay, [egl.EGLenum, POINTER(None), POINTER(egl.EGLint)], None)
+eglQueryDevicesEXT = _link_function('eglQueryDevicesEXT', egl.EGLBoolean, [egl.EGLint, POINTER(EGLDeviceEXT), POINTER(egl.EGLint)], None)
+
+__all__ = ['EGL_PLATFORM_DEVICE_EXT', 'EGLDeviceEXT', 'eglGetPlatformDisplayEXT', 'eglQueryDevicesEXT']
diff --git a/pyglet/libs/egl/lib.py b/pyglet/libs/egl/lib.py
new file mode 100644
index 0000000..0707518
--- /dev/null
+++ b/pyglet/libs/egl/lib.py
@@ -0,0 +1,31 @@
+from ctypes import *
+
+import pyglet
+import pyglet.util
+
+
+__all__ = ['link_EGL']
+
+egl_lib = pyglet.lib.load_library('EGL')
+
+# Look for eglGetProcAddress
+eglGetProcAddress = getattr(egl_lib, 'eglGetProcAddress')
+eglGetProcAddress.restype = POINTER(CFUNCTYPE(None))
+eglGetProcAddress.argtypes = [POINTER(c_ubyte)]
+
+
+def link_EGL(name, restype, argtypes, requires=None, suggestions=None):
+    try:
+        func = getattr(egl_lib, name)
+        func.restype = restype
+        func.argtypes = argtypes
+        return func
+    except AttributeError:
+        bname = cast(pointer(create_string_buffer(pyglet.util.asbytes(name))), POINTER(c_ubyte))
+        addr = eglGetProcAddress(bname)
+        if addr:
+            ftype = CFUNCTYPE(*((restype,) + tuple(argtypes)))
+            func = cast(addr, ftype)
+            return func
+
+    return pyglet.gl.lib.missing_function(name, requires, suggestions)
diff --git a/pyglet/libs/win32/__init__.py b/pyglet/libs/win32/__init__.py
index 34e3393..62ba731 100755
--- a/pyglet/libs/win32/__init__.py
+++ b/pyglet/libs/win32/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -107,6 +107,8 @@ _gdi32.CreateDIBSection.restype = HBITMAP
 _gdi32.CreateDIBSection.argtypes = [HDC, c_void_p, UINT, c_void_p, HANDLE, DWORD]  # POINTER(BITMAPINFO)
 _gdi32.CreateFontIndirectA.restype = HFONT
 _gdi32.CreateFontIndirectA.argtypes = [POINTER(LOGFONT)]
+_gdi32.CreateFontIndirectW.restype = HFONT
+_gdi32.CreateFontIndirectW.argtypes = [POINTER(LOGFONTW)]
 _gdi32.DeleteDC.restype = BOOL
 _gdi32.DeleteDC.argtypes = [HDC]
 _gdi32.DeleteObject.restype = BOOL
diff --git a/pyglet/libs/win32/constants.py b/pyglet/libs/win32/constants.py
index 3ed8de4..25f94e9 100644
--- a/pyglet/libs/win32/constants.py
+++ b/pyglet/libs/win32/constants.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -5087,3 +5087,8 @@ MF_FILEFLAGS_NONE = 0
 MF_FILEFLAGS_NOBUFFERING = 1
 
 CLSCTX_INPROC_SERVER = 0x1
+
+# From Dwmapi.h
+DWM_BB_ENABLE = 0x00000001
+DWM_BB_BLURREGION = 0x00000002
+DWM_BB_TRANSITIONONMAXIMIZED = 0x00000004
diff --git a/pyglet/libs/win32/dinput.py b/pyglet/libs/win32/dinput.py
index 44ece2f..f083b16 100755
--- a/pyglet/libs/win32/dinput.py
+++ b/pyglet/libs/win32/dinput.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/libs/win32/libwintab.py b/pyglet/libs/win32/libwintab.py
index 1b3e1ab..e001f70 100755
--- a/pyglet/libs/win32/libwintab.py
+++ b/pyglet/libs/win32/libwintab.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/libs/win32/types.py b/pyglet/libs/win32/types.py
index 91c5964..6c51cce 100644
--- a/pyglet/libs/win32/types.py
+++ b/pyglet/libs/win32/types.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -267,6 +267,25 @@ class LOGFONT(Structure):
     ]
 
 
+class LOGFONTW(Structure):
+    _fields_ = [
+        ('lfHeight', LONG),
+        ('lfWidth', LONG),
+        ('lfEscapement', LONG),
+        ('lfOrientation', LONG),
+        ('lfWeight', LONG),
+        ('lfItalic', BYTE),
+        ('lfUnderline', BYTE),
+        ('lfStrikeOut', BYTE),
+        ('lfCharSet', BYTE),
+        ('lfOutPrecision', BYTE),
+        ('lfClipPrecision', BYTE),
+        ('lfQuality', BYTE),
+        ('lfPitchAndFamily', BYTE),
+        ('lfFaceName', (WCHAR * LF_FACESIZE))
+    ]
+
+
 class TRACKMOUSEEVENT(Structure):
     _fields_ = [
         ('cbSize', DWORD),
@@ -334,15 +353,8 @@ class MONITORINFOEX(Structure):
     __slots__ = [f[0] for f in _fields_]
 
 
-class DEVMODE(Structure):
+class _DUMMYSTRUCTNAME(Structure):
     _fields_ = [
-        ('dmDeviceName', BCHAR * CCHDEVICENAME),
-        ('dmSpecVersion', WORD),
-        ('dmDriverVersion', WORD),
-        ('dmSize', WORD),
-        ('dmDriverExtra', WORD),
-        ('dmFields', DWORD),
-        # Just using largest union member here
         ('dmOrientation', c_short),
         ('dmPaperSize', c_short),
         ('dmPaperLength', c_short),
@@ -351,6 +363,34 @@ class DEVMODE(Structure):
         ('dmCopies', c_short),
         ('dmDefaultSource', c_short),
         ('dmPrintQuality', c_short),
+    ]
+
+class _DUMMYSTRUCTNAME2(Structure):
+    _fields_ = [
+        ('dmPosition', POINTL),
+        ('dmDisplayOrientation', DWORD),
+        ('dmDisplayFixedOutput', DWORD)
+    ]
+
+class _DUMMYDEVUNION(Union):
+    _anonymous_ = ('_dummystruct1', '_dummystruct2')
+    _fields_ = [
+        ('_dummystruct1', _DUMMYSTRUCTNAME),
+        ('dmPosition', POINTL),
+        ('_dummystruct2', _DUMMYSTRUCTNAME2),
+    ]
+
+class DEVMODE(Structure):
+    _anonymous_ = ('_dummyUnion',)
+    _fields_ = [
+        ('dmDeviceName', BCHAR * CCHDEVICENAME),
+        ('dmSpecVersion', WORD),
+        ('dmDriverVersion', WORD),
+        ('dmSize', WORD),
+        ('dmDriverExtra', WORD),
+        ('dmFields', DWORD),
+        # Just using largest union member here
+        ('_dummyUnion', _DUMMYDEVUNION),
         # End union
         ('dmColor', c_short),
         ('dmDuplex', c_short),
@@ -488,3 +528,11 @@ class PROPVARIANT(ctypes.Structure):
         ('union', _VarTable)
     ]
 
+
+class DWM_BLURBEHIND(ctypes.Structure):
+    _fields_ = [
+        ("dwFlags", DWORD),
+        ("fEnable", BOOL),
+        ("hRgnBlur", HRGN),
+        ("fTransitionOnMaximized", DWORD),
+    ]
diff --git a/pyglet/libs/win32/winkey.py b/pyglet/libs/win32/winkey.py
index a9af383..d1ed999 100644
--- a/pyglet/libs/win32/winkey.py
+++ b/pyglet/libs/win32/winkey.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/libs/x11/cursorfont.py b/pyglet/libs/x11/cursorfont.py
index 20dab16..787ff75 100644
--- a/pyglet/libs/x11/cursorfont.py
+++ b/pyglet/libs/x11/cursorfont.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/libs/x11/xf86vmode.py b/pyglet/libs/x11/xf86vmode.py
index 629ec94..8d2c0b3 100644
--- a/pyglet/libs/x11/xf86vmode.py
+++ b/pyglet/libs/x11/xf86vmode.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/libs/x11/xinerama.py b/pyglet/libs/x11/xinerama.py
index cbd4fb2..af910d5 100644
--- a/pyglet/libs/x11/xinerama.py
+++ b/pyglet/libs/x11/xinerama.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/libs/x11/xinput.py b/pyglet/libs/x11/xinput.py
index 40a2028..4f873fd 100644
--- a/pyglet/libs/x11/xinput.py
+++ b/pyglet/libs/x11/xinput.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/libs/x11/xlib.py b/pyglet/libs/x11/xlib.py
index f839412..02a5ffb 100644
--- a/pyglet/libs/x11/xlib.py
+++ b/pyglet/libs/x11/xlib.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/libs/x11/xsync.py b/pyglet/libs/x11/xsync.py
index 8fc49ba..e85d7e1 100644
--- a/pyglet/libs/x11/xsync.py
+++ b/pyglet/libs/x11/xsync.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/math.py b/pyglet/math.py
index 07affb8..0b9bb8e 100644
--- a/pyglet/math.py
+++ b/pyglet/math.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -33,79 +33,680 @@
 # POSSIBILITY OF SUCH DAMAGE.
 # ----------------------------------------------------------------------------
 
-"""Matrix and Vector operations.
-
-This module provides classes for Matrix and Vector math.
-A :py:class:`~pyglet.matrix.Mat4` class is available for representing
-4x4 matricies, including helper methods for rotating, scaling, and
-transforming. The internal datatype of :py:class:`~pyglet.matrix.Mat4`
-is a 1-dimensional array, so instances can be passed directly to OpenGL.
+"""Matrix and Vector math.
 
+This module provides Vector and Matrix objects, include Vec2, Vec3, Vec4,
+Mat3 and Mat4. Most common operations are supported, and many helper
+methods are included for rotating, scaling, and transforming.
+The :py:class:`~pyglet.matrix.Mat4` includes class methods
+for creating orthographic and perspective projection matrixes.
 """
 
 import math as _math
-import operator as _operator
 import warnings as _warnings
+from operator import mul as _mul
+
+
+def clamp(num, min_val, max_val):
+    return max(min(num, max_val), min_val)
+
+
+class Vec2(tuple):
+    """A two dimensional vector represented as an X Y coordinate pair.
+
+    :parameters:
+        `x` : int or float :
+            The X coordinate of the vector.
+        `y`   : int or float :
+            The Y coordinate of the vector.
+
+    Vectors must be created with either 0 or 2 values. If no arguments are provided a vector with the coordinates 0, 0 is created.
+
+    Vectors are stored as a tuple and therefore immutable and cannot be modified directly
+    """
+
+    def __new__(cls, *args):
+        assert len(args) in (0, 2), "0 or 2 values are required for Vec2 types."
+        return super().__new__(Vec2, args or (0, 0))
+
+    @staticmethod
+    def from_polar(mag, angle):
+        """Create a new vector from the given polar coodinates.
+
+        :parameters:
+            `mag`   : int or float :
+                The magnitude of the vector.
+            `angle` : int or float :
+                The angle of the vector in radians.
+
+        :returns: A new vector with the given angle and magnitude.
+        :rtype: Vec2
+        """
+        return Vec2(mag * _math.cos(angle), mag * _math.sin(angle))
+
+    @property
+    def x(self):
+        """The X coordinate of the vector.
+
+        :type: float
+        """
+        return self[0]
+
+    @property
+    def y(self):
+        """The Y coordinate of the vector.
+
+        :type: float
+        """
+        return self[1]
+
+    @property
+    def heading(self):
+        """The angle of the vector in radians.
+
+        :type: float
+        """
+        return _math.atan2(self[1], self[0])
+
+    @property
+    def mag(self):
+        """The magnitude, or length of the vector. The distance between the coordinates and the origin.
+
+        Alias of abs(self).
+
+        :type: float
+        """
+        return self.__abs__()
+
+    def __add__(self, other):
+        return Vec2(self[0] + other[0], self[1] + other[1])
+
+    def __sub__(self, other):
+        return Vec2(self[0] - other[0], self[1] - other[1])
+
+    def __mul__(self, other):
+        return Vec2(self[0] * other[0], self[1] * other[1])
+
+    def __truediv__(self, other):
+        return Vec2(self[0] / other[0], self[1] / other[1])
+
+    def __abs__(self):
+        return _math.sqrt(self[0] ** 2 + self[1] ** 2)
+
+    def __neg__(self):
+        return Vec2(-self[0], -self[1])
+
+    def __round__(self, ndigits=None):
+        return Vec2(*(round(v, ndigits) for v in self))
+
+    def __radd__(self, other):
+        """Reverse add. Required for functionality with sum()
+        """
+        if other == 0:
+            return self
+        else:
+            return self.__add__(other)
+
+    def from_magnitude(self, magnitude):
+        """Create a new Vector of the given magnitude by normalizing, then scaling the vector. The heading remains unchanged.
+
+        :parameters:
+            `magnitude` : int or float :
+                The magnitude of the new vector.
+
+        :returns: A new vector with the magnitude.
+        :rtype: Vec2
+        """
+        return self.normalize().scale(magnitude)
+
+    def from_heading(self, heading):
+        """Create a new vector of the same magnitude with the given heading. I.e. Rotate the vector to the heading.
+
+        :parameters:
+            `heading` : int or float :
+                The angle of the new vector in radians.
+
+        :returns: A new vector with the given heading.
+        :rtype: Vec2
+        """
+        mag = self.__abs__()
+        return Vec2(mag * _math.cos(heading), mag * _math.sin(heading))
+
+    def limit(self, max):
+        """Limit the magnitude of the vector to the value used for the max parameter.
+
+        :parameters:
+            `max`  : int or float :
+                The maximum magnitude for the vector.
+
+        :returns: Either self or a new vector with the maximum magnitude.
+        :rtype: Vec2
+        """
+        if self[0] ** 2 + self[1] ** 2 > max * max:
+            return self.from_magnitude(max)
+        return self
+
+    def lerp(self, other, alpha):
+        """Create a new vector lineraly interpolated between this vector and another vector.
+
+        :parameters:
+            `other`  : Vec2 :
+                The vector to be linerly interpolated to.
+            `alpha` : float or int :
+                The amount of interpolation.
+                Some value between 0.0 (this vector) and 1.0 (other vector).
+                0.5 is halfway inbetween.
+
+        :returns: A new interpolated vector.
+        :rtype: Vec2
+        """
+        return Vec2(self[0] + (alpha * (other[0] - self[0])),
+                    self[1] + (alpha * (other[1] - self[1])))
+
+    def scale(self, value):
+        """Multiply the vector by a scalar value.
+
+        :parameters:
+            `value`  : int or float :
+                The ammount to be scaled by
+
+        :returns: A new vector scaled by the value.
+        :rtype: Vec2
+        """
+        return Vec2(self[0] * value, self[1] * value)
+
+    def rotate(self, angle):
+        """Create a new Vector rotated by the angle. The magnitude remains unchanged.
+
+        :parameters:
+            `angle` : int or float :
+                The angle to rotate by
+
+        :returns: A new rotated vector of the same magnitude.
+        :rtype: Vec2
+        """
+        mag = self.mag
+        heading = self.heading
+        return Vec2(mag * _math.cos(heading + angle), mag * _math.sin(heading+angle))
+
+    def distance(self, other):
+        """Calculate the distance between this vector and another 2D vector.
+
+        :parameters:
+            `other`  : Vec2 :
+                The other vector
+
+        :returns: The distance between the two vectors.
+        :rtype: float
+        """
+        return _math.sqrt(((other[0] - self[0]) ** 2) + ((other[1] - self[1]) ** 2))
+
+    def normalize(self):
+        """Normalize the vector to have a magnitude of 1. i.e. make it a unit vector.
+
+        :returns: A unit vector with the same heading.
+        :rtype: Vec2
+        """
+        d = self.__abs__()
+        if d:
+            return Vec2(self[0] / d, self[1] / d)
+        return self
+
+    def clamp(self, min_val, max_val):
+        """Restrict the value of the X and Y components of the vector to be within the given values.
+
+        :parameters:
+            `min_val` : int or float :
+                The minimum value
+            `max_val` : int or float :
+                The maximum value
+
+        :returns: A new vector with clamped X and Y components.
+        :rtype: Vec2
+        """
+        return Vec2(clamp(self[0], min_val, max_val), clamp(self[1], min_val, max_val))
+
+    def dot(self, other):
+        """Calculate the dot product of this vector and another 2D vector.
+
+        :parameters:
+            `other`  : Vec2 :
+                The other vector.
+
+        :returns: The dot product of the two vectors.
+        :rtype: float
+        """
+        return self[0] * other[0] + self[1] * other[1]
+
+    def __getattr__(self, attrs):
+        try:
+            # Allow swizzed getting of attrs
+            vec_class = {2: Vec2, 3: Vec3, 4: Vec4}.get(len(attrs))
+            return vec_class(*(self['xy'.index(c)] for c in attrs))
+        except Exception:
+            raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attrs}'")
+
+    def __repr__(self):
+        return f"Vec2({self[0]}, {self[1]})"
+
+
+class Vec3(tuple):
+    """A three dimensional vector represented as a X Y Z coordinates.
+
+    :parameters:
+        `x` : int or float :
+            The X coordinate of the vector.
+        `y`   : int or float :
+            The Y coordinate of the vector.
+        `z`   : int or float :
+            The Z coordinate of the vector.
+
+    3D Vectors must be created with either 0 or 3 values. If no arguments are provided a vector with the coordinates 0, 0, 0 is created.
+
+    Vectors are stored as a tuple and therefore immutable and cannot be modified directly
+    """
+
+    def __new__(cls, *args):
+        assert len(args) in (0, 3), "0 or 3 values are required for Vec3 types."
+        return super().__new__(Vec3, args or (0, 0, 0))
+
+    @property
+    def x(self):
+        """The X coordinate of the vector.
+
+        :type: float
+        """
+        return self[0]
+
+    @property
+    def y(self):
+        """The Y coordinate of the vector.
+
+        :type: float
+        """
+        return self[1]
+
+    @property
+    def z(self):
+        """The Z coordinate of the vector.
+
+        :type: float
+        """
+        return self[2]
+
+    @property
+    def mag(self):
+        """The magnitude, or length of the vector. The distance between the coordinates and the origin.
+
+        Alias of abs(self).
+
+        :type: float
+        """
+        return self.__abs__()
+
+    def __add__(self, other):
+        return Vec3(self[0] + other[0], self[1] + other[1], self[2] + other[2])
+
+    def __sub__(self, other):
+        return Vec3(self[0] - other[0], self[1] - other[1], self[2] - other[2])
+
+    def __mul__(self, other):
+        return Vec3(self[0] * other[0], self[1] * other[1], self[2] * other[2])
+
+    def __truediv__(self, other):
+        return Vec3(self[0] / other[0], self[1] / other[1], self[2] / other[2])
 
+    def __abs__(self):
+        return _math.sqrt(self[0] ** 2 + self[1] ** 2 + self[2] ** 2)
 
-def create_orthogonal(left, right, bottom, top, znear, zfar):
-    """Create a Mat4 orthographic projection matrix."""
-    width = right - left
-    height = top - bottom
-    depth = zfar - znear
+    def __neg__(self):
+        return Vec3(-self[0], -self[1], -self[2])
+
+    def __round__(self, ndigits=None):
+        return Vec3(*(round(v, ndigits) for v in self))
+
+    def __radd__(self, other):
+        """Reverse add. Required for functionality with sum()
+        """
+        if other == 0:
+            return self
+        else:
+            return self.__add__(other)
+
+    def from_magnitude(self, magnitude):
+        """Create a new Vector of the given magnitude by normalizing, then scaling the vector. The rotation remains unchanged.
+
+        :parameters:
+            `magnitude` : int or float :
+                The magnitude of the new vector.
+
+        :returns: A new vector with the magnitude.
+        :rtype: Vec3
+        """
+        return self.normalize().scale(magnitude)
+
+    def limit(self, max):
+        """Limit the magnitude of the vector to the value used for the max parameter.
+
+        :parameters:
+            `max`  : int or float :
+                The maximum magnitude for the vector.
+
+        :returns: Either self or a new vector with the maximum magnitude.
+        :rtype: Vec3
+        """
+        if self[0] ** 2 + self[1] ** 2 + self[2] **2 > max * max * max:
+            return self.from_magnitude(max)
+        return self
+
+    def cross(self, other):
+        """Calculate the cross product of this vector and another 3D vector.
+
+        :parameters:
+            `other`  : Vec3 :
+                The other vector.
+
+        :returns: The cross product of the two vectors.
+        :rtype: float
+        """
+        return Vec3((self[1] * other[2]) - (self[2] * other[1]),
+                    (self[2] * other[0]) - (self[0] * other[2]),
+                    (self[0] * other[1]) - (self[1] * other[0]))
+
+    def dot(self, other):
+        """Calculate the dot product of this vector and another 3D vector.
+
+        :parameters:
+            `other`  : Vec3 :
+                The other vector.
+
+        :returns: The dot product of the two vectors.
+        :rtype: float
+        """
+        return self[0] * other[0] + self[1] * other[1] + self[2] * other[2]
+
+    def lerp(self, other, alpha):
+        """Create a new vector lineraly interpolated between this vector and another vector.
+
+        :parameters:
+            `other`  : Vec3 :
+                The vector to be linerly interpolated to.
+            `alpha` : float or int :
+                The amount of interpolation.
+                Some value between 0.0 (this vector) and 1.0 (other vector).
+                0.5 is halfway inbetween.
+
+        :returns: A new interpolated vector.
+        :rtype: Vec3
+        """
+        return Vec3(self[0] + (alpha * (other[0] - self[0])),
+                    self[1] + (alpha * (other[1] - self[1])),
+                    self[2] + (alpha * (other[2] - self[2])))
+
+    def scale(self, value):
+        """Multiply the vector by a scalar value.
+
+        :parameters:
+            `value`  : int or float :
+                The ammount to be scaled by
+
+        :returns: A new vector scaled by the value.
+        :rtype: Vec3
+        """
+        return Vec3(self[0] * value, self[1] * value, self[2] * value)
+
+    def distance(self, other):
+        """Calculate the distance between this vector and another 3D vector.
+
+        :parameters:
+            `other`  : Vec3 :
+                The other vector
+
+        :returns: The distance between the two vectors.
+        :rtype: float
+        """
+        return _math.sqrt(((other[0] - self[0]) ** 2) +
+                          ((other[1] - self[1]) ** 2) +
+                          ((other[2] - self[2]) ** 2))
+
+    def normalize(self):
+        """Normalize the vector to have a magnitude of 1. i.e. make it a unit vector.
+
+        :returns: A unit vector with the same rotation.
+        :rtype: Vec3
+        """
+        d = self.__abs__()
+        if d:
+            return Vec3(self[0] / d, self[1] / d, self[2] / d)
+        return self
+
+    def clamp(self, min_val, max_val):
+        """Restrict the value of the X,  Y and Z components of the vector to be within the given values.
+
+        :parameters:
+            `min_val` : int or float :
+                The minimum value
+            `max_val` : int or float :
+                The maximum value
+
+        :returns: A new vector with clamped X, Y and Z components.
+        :rtype: Vec3
+        """
+        return Vec3(clamp(self[0], min_val, max_val),
+                    clamp(self[1], min_val, max_val),
+                    clamp(self[2], min_val, max_val))
+
+    def __getattr__(self, attrs):
+        try:
+            # Allow swizzed getting of attrs
+            vec_class = {2: Vec2, 3: Vec3, 4: Vec4}.get(len(attrs))
+            return vec_class(*(self['xyz'.index(c)] for c in attrs))
+        except Exception:
+            raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attrs}'")
+
+    def __repr__(self):
+        return f"Vec3({self[0]}, {self[1]}, {self[2]})"
+
+
+class Vec4(tuple):
+
+    def __new__(cls, *args):
+        assert len(args) in (0, 4), "0 or 4 values are required for Vec4 types."
+        return super().__new__(Vec4, args or (0, 0, 0, 0))
+
+    @property
+    def x(self):
+        return self[0]
+
+    @property
+    def y(self):
+        return self[1]
+
+    @property
+    def z(self):
+        return self[2]
+
+    @property
+    def w(self):
+        return self[3]
+
+    def __add__(self, other):
+        return Vec4(self[0] + other[0], self[1] + other[1], self[2] + other[2], self[3] + other[3])
+
+    def __sub__(self, other):
+        return Vec4(self[0] - other[0], self[1] - other[1], self[2] - other[2], self[3] - other[3])
+
+    def __mul__(self, other):
+        return Vec4(self[0] * other[0], self[1] * other[1], self[2] * other[2], self[3] * other[3])
+
+    def __truediv__(self, other):
+        return Vec4(self[0] / other[0], self[1] / other[1], self[2] / other[2], self[3] / other[3])
+
+    def __abs__(self):
+        return _math.sqrt(self[0] ** 2 + self[1] ** 2 + self[2] ** 2 + self[3] ** 2)
+
+    def __neg__(self):
+        return Vec4(-self[0], -self[1], -self[2], -self[3])
+
+    def __round__(self, ndigits=None):
+        return Vec4(*(round(v, ndigits) for v in self))
+
+    def __radd__(self, other):
+        if other == 0:
+            return self
+        else:
+            return self.__add__(other)
+
+    def lerp(self, other, alpha):
+        return Vec4(self[0] + (alpha * (other[0] - self[0])),
+                    self[1] + (alpha * (other[1] - self[1])),
+                    self[2] + (alpha * (other[2] - self[2])),
+                    self[3] + (alpha * (other[3] - self[3])))
+
+    def scale(self, value):
+        return Vec4(self[0] * value, self[1] * value, self[2] * value, self[3] * value)
+
+    def distance(self, other):
+        return _math.sqrt(((other[0] - self[0]) ** 2) +
+                          ((other[1] - self[1]) ** 2) +
+                          ((other[2] - self[2]) ** 2) +
+                          ((other[3] - self[3]) ** 2))
+
+    def normalize(self):
+        d = self.__abs__()
+        if d:
+            return Vec4(self[0] / d, self[1] / d, self[2] / d, self[3] / d)
+        return self
+
+    def clamp(self, min_val, max_val):
+        return Vec4(clamp(self[0], min_val, max_val),
+                    clamp(self[1], min_val, max_val),
+                    clamp(self[2], min_val, max_val),
+                    clamp(self[3], min_val, max_val))
+
+    def dot(self, other):
+        return self[0] * other[0] + self[1] * other[1] + self[2] * other[2] + self[3] * other[3]
+
+    def __getattr__(self, attrs):
+        try:
+            # Allow swizzed getting of attrs
+            vec_class = {2: Vec2, 3: Vec3, 4: Vec4}.get(len(attrs))
+            return vec_class(*(self['xyzw'.index(c)] for c in attrs))
+        except Exception:
+            raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attrs}'")
+
+    def __repr__(self):
+        return f"Vec4({self[0]}, {self[1]}, {self[2]}, {self[3]})"
+
+
+class Mat3(tuple):
+    """A 3x3 Matrix class
+
+    `Mat3` is an immutable 3x3 Matrix, including most common
+    operators. Matrix multiplication must be performed using
+    the "@" operator.
+    """
+
+    def __new__(cls, values=None) -> 'Mat3':
+        """Create a 3x3 Matrix
+
+        A Mat3 can be created with a list or tuple of 9 values.
+        If no values are provided, an "identity matrix" will be created
+        (1.0 on the main diagonal). Matrix objects are immutable, so
+        all operations return a new Mat3 object.
+
+        :Parameters:
+            `values` : tuple of float or int
+                A tuple or list containing 9 floats or ints.
+        """
+        assert values is None or len(values) == 9, "A 3x3 Matrix requires 9 values"
+        return super().__new__(Mat3, values or (1.0, 0.0, 0.0,
+                                                0.0, 1.0, 0.0,
+                                                0.0, 0.0, 1.0))
+
+    def scale(self, sx: float, sy: float):
+        return self @ (1.0 / sx, 0.0, 0.0, 0.0, 1.0 / sy, 0.0, 0.0, 0.0, 1.0)
+
+    def translate(self, tx: float, ty: float):
+        return self @ (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, -tx, ty, 1.0)
 
-    sx = 2.0 / width
-    sy = 2.0 / height
-    sz = 2.0 / -depth
+    def rotate(self, phi: float):
+        s = _math.sin(_math.radians(phi))
+        c = _math.cos(_math.radians(phi))
+        return self @ (c, s, 0.0, -s, c, 0.0, 0.0, 0.0, 1.0)
 
-    tx = -(right + left) / width
-    ty = -(top + bottom) / height
-    tz = -(zfar + znear) / depth
+    def shear(self, sx: float, sy: float):
+        return self @ (1.0, sy, 0.0, sx, 1.0, 0.0, 0.0, 0.0, 1.0)
 
-    return Mat4((sx, 0.0, 0.0, 0.0,
-                 0.0, sy, 0.0, 0.0,
-                 0.0, 0.0, sz, 0.0,
-                 tx, ty, tz, 1.0))
+    def __add__(self, other) -> 'Mat3':
+        assert len(other) == 9, "Can only add to other Mat3 types"
+        return Mat3(tuple(s + o for s, o in zip(self, other)))
 
+    def __sub__(self, other) -> 'Mat3':
+        assert len(other) == 9, "Can only subtract from other Mat3 types"
+        return Mat3(tuple(s - o for s, o in zip(self, other)))
 
-def create_perspective(left, right, bottom, top, znear, zfar, fov=60):
-    """Create a Mat4 perspective projection matrix."""
-    width = right - left
-    height = top - bottom
-    aspect = width / height
+    def __pos__(self):
+        return self
+
+    def __neg__(self) -> 'Mat3':
+        return Mat3(tuple(-v for v in self))
+
+    def __round__(self, ndigits=None) -> 'Mat3':
+        return Mat3(tuple(round(v, ndigits) for v in self))
+
+    def __mul__(self, other):
+        raise NotImplementedError("Please use the @ operator for Matrix multiplication.")
 
-    xymax = znear * _math.tan(fov * _math.pi / 360)
-    ymin = -xymax
-    xmin = -xymax
+    def __matmul__(self, other) -> 'Mat3':
+        assert len(other) in (3, 9), "Can only multiply with Mat3 or Vec3 types"
 
-    width = xymax - xmin
-    height = xymax - ymin
-    depth = zfar - znear
-    q = -(zfar + znear) / depth
-    qn = -2 * zfar * znear / depth
+        if type(other) is Vec3:
+            # Columns:
+            c0 = self[0::3]
+            c1 = self[1::3]
+            c2 = self[2::3]
+            return Vec3(sum(map(_mul, c0, other)),
+                        sum(map(_mul, c1, other)),
+                        sum(map(_mul, c2, other)))
 
-    w = 2 * znear / width
-    w = w / aspect
-    h = 2 * znear / height
+        # Rows:
+        r0 = self[0:3]
+        r1 = self[3:6]
+        r2 = self[6:9]
+        # Columns:
+        c0 = other[0::3]
+        c1 = other[1::3]
+        c2 = other[2::3]
+
+        # Multiply and sum rows * colums:
+        return Mat3((sum(map(_mul, r0, c0)),
+                     sum(map(_mul, r0, c1)),
+                     sum(map(_mul, r0, c2)),
 
-    return Mat4((w, 0, 0, 0,
-                 0, h, 0, 0,
-                 0, 0, q, -1,
-                 0, 0, qn, 0))
+                     sum(map(_mul, r1, c0)),
+                     sum(map(_mul, r1, c1)),
+                     sum(map(_mul, r1, c2)),
+
+                     sum(map(_mul, r2, c0)),
+                     sum(map(_mul, r2, c1)),
+                     sum(map(_mul, r2, c2))))
+
+    def __repr__(self) -> str:
+        return f"{self.__class__.__name__}{self[0:3]}\n    {self[3:6]}\n    {self[6:9]}"
 
 
 class Mat4(tuple):
-    """A 4x4 Matrix
-
-    `Mat4` is a simple immutable 4x4 Matrix, with a few operators.
-    Two types of multiplication are possible. The "*" operator
-    will perform elementwise multiplication, wheras the "@"
-    operator will perform Matrix multiplication. Internally,
-    data is stored in a linear 1D array, allowing direct passing
-    to OpenGL.
+    """A 4x4 Matrix class
+
+    `Mat4` is an immutable 4x4 Matrix, including most common
+    operators. Matrix multiplication must be performed using
+    the "@" operator.
+    Class methods are available for creating orthogonal
+    and perspective projections matrixes.
     """
 
-    def __new__(cls, values=None):
+    def __new__(cls, values=None) -> 'Mat4':
         """Create a 4x4 Matrix
 
         A Matrix can be created with a list or tuple of 16 values.
@@ -123,43 +724,132 @@ class Mat4(tuple):
                                                 0.0, 0.0, 1.0, 0.0,
                                                 0.0, 0.0, 0.0, 1.0))
 
-    def row(self, index):
+    @classmethod
+    def orthogonal_projection(cls, left, right, bottom, top, z_near, z_far) -> 'Mat4':
+        """Create a Mat4 orthographic projection matrix."""
+        width = right - left
+        height = top - bottom
+        depth = z_far - z_near
+
+        sx = 2.0 / width
+        sy = 2.0 / height
+        sz = 2.0 / -depth
+
+        tx = -(right + left) / width
+        ty = -(top + bottom) / height
+        tz = -(z_far + z_near) / depth
+
+        return cls((sx, 0.0, 0.0, 0.0,
+                    0.0, sy, 0.0, 0.0,
+                    0.0, 0.0, sz, 0.0,
+                    tx, ty, tz, 1.0))
+
+    @classmethod
+    def perspective_projection(cls, left, right, bottom, top, z_near, z_far, fov=60) -> 'Mat4':
+        """Create a Mat4 perspective projection matrix."""
+        width = right - left
+        height = top - bottom
+        aspect = width / height
+
+        xy_max = z_near * _math.tan(fov * _math.pi / 360)
+        y_min = -xy_max
+        x_min = -xy_max
+
+        width = xy_max - x_min
+        height = xy_max - y_min
+        depth = z_far - z_near
+        q = -(z_far + z_near) / depth
+        qn = -2 * z_far * z_near / depth
+
+        w = 2 * z_near / width
+        w = w / aspect
+        h = 2 * z_near / height
+
+        return cls((w, 0, 0, 0,
+                   0, h, 0, 0,
+                   0, 0, q, -1,
+                   0, 0, qn, 0))
+
+    @classmethod
+    def from_translation(cls, vector: Vec3) -> 'Mat4':
+        """Create a translaton matrix from a Vec3.
+
+        :Parameters:
+            `vector` : A `Vec3`, or 3 component tuple of float or int
+                Vec3 or tuple with x, y and z translaton values
+        """
+        return cls((1.0, 0.0, 0.0, 0.0,
+                    0.0, 1.0, 0.0, 0.0,
+                    0.0, 0.0, 1.0, 0.0,
+                    vector[0], vector[1], vector[2], 1.0))
+
+    @classmethod
+    def from_rotation(cls, angle: float, vector: Vec3) -> 'Mat4':
+        """Create a rotation matrix from an angle and Vec3.
+
+        :Parameters:
+            `angle` : A `float`
+            `vector` : A `Vec3`, or 3 component tuple of float or int
+                Vec3 or tuple with x, y and z translaton values
+        """
+        return cls().rotate(angle, vector)
+
+    @classmethod
+    def look_at_direction(cls, direction: Vec3, up: Vec3) -> 'Mat4':
+        vec_z = direction.normalize()
+        vec_x = direction.cross_product(up).normalize()
+        vec_y = direction.cross_product(vec_z).normalize()
+
+        return cls((vec_x.x, vec_y.x, vec_z.x, 0.0,
+                    vec_x.y, vec_y.y, vec_z.y, 0.0,
+                    vec_x.z, vec_z.z, vec_z.z, 0.0,
+                    0.0, 0.0, 0.0, 1.0))
+
+    @classmethod
+    def look_at(cls, position: Vec3, target: Vec3, up: Vec3) -> 'Mat4':
+        direction = target - position
+        direction_mat4 = cls.look_at_direction(direction, up)
+        position_mat4 = cls.from_translation(position.negate())
+        return direction_mat4 @ position_mat4
+
+    def row(self, index: int):
         """Get a specific row as a tuple."""
         return self[index*4:index*4+4]
 
-    def column(self, index):
+    def column(self, index: int):
         """Get a specific column as a tuple."""
         return self[index::4]
 
-    def scale(self, x=1, y=1, z=1):
+    def scale(self, vector: Vec3) -> 'Mat4':
         """Get a scale Matrix on x, y, or z axis."""
         temp = list(self)
-        temp[0] *= x
-        temp[5] *= y
-        temp[10] *= z
+        temp[0] *= vector[0]
+        temp[5] *= vector[1]
+        temp[10] *= vector[2]
         return Mat4(temp)
 
-    def translate(self, x=0, y=0, z=0):
+    def translate(self, vector: Vec3) -> 'Mat4':
         """Get a translate Matrix along x, y, and z axis."""
-        return Mat4(self) @ Mat4((1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1))
+        return Mat4(self) @ Mat4((1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, *vector, 1))
 
-    def rotate(self, angle=0, x=0, y=0, z=0):
+    def rotate(self, angle: float, vector: Vec3) -> 'Mat4':
         """Get a rotation Matrix on x, y, or z axis."""
-        assert all(abs(n) <= 1 for n in (x, y, z)), "x,y,z must be normalized (<=1)"
+        assert all(abs(n) <= 1 for n in vector), "vector must be normalized (<=1)"
+        x, y, z = vector
         c = _math.cos(angle)
         s = _math.sin(angle)
         t = 1 - c
-        tempx, tempy, tempz = t * x, t * y, t * z
-
-        ra = c + tempx * x
-        rb = 0 + tempx * y + s * z
-        rc = 0 + tempx * z - s * y
-        re = 0 + tempy * x - s * z
-        rf = c + tempy * y
-        rg = 0 + tempy * z + s * x
-        ri = 0 + tempz * x + s * y
-        rj = 0 + tempz * y - s * x
-        rk = c + tempz * z
+        temp_x, temp_y, temp_z = t * x, t * y, t * z
+
+        ra = c + temp_x * x
+        rb = 0 + temp_x * y + s * z
+        rc = 0 + temp_x * z - s * y
+        re = 0 + temp_y * x - s * z
+        rf = c + temp_y * y
+        rg = 0 + temp_y * z + s * x
+        ri = 0 + temp_z * x + s * y
+        rj = 0 + temp_z * y - s * x
+        rk = c + temp_z * z
 
         # ra, rb, rc, --
         # re, rf, rg, --
@@ -168,25 +858,25 @@ class Mat4(tuple):
 
         return Mat4(self) @ Mat4((ra, rb, rc, 0, re, rf, rg, 0, ri, rj, rk, 0, 0, 0, 0, 1))
 
-    def transpose(self):
+    def transpose(self) -> 'Mat4':
         """Get a tranpose of this Matrix."""
         return Mat4(self[0::4] + self[1::4] + self[2::4] + self[3::4])
 
-    def __add__(self, other):
+    def __add__(self, other) -> 'Mat4':
         assert len(other) == 16, "Can only add to other Mat4 types"
         return Mat4(tuple(s + o for s, o in zip(self, other)))
 
-    def __sub__(self, other):
+    def __sub__(self, other) -> 'Mat4':
         assert len(other) == 16, "Can only subtract from other Mat4 types"
         return Mat4(tuple(s - o for s, o in zip(self, other)))
 
     def __pos__(self):
         return self
 
-    def __neg__(self):
+    def __neg__(self) -> 'Mat4':
         return Mat4(tuple(-v for v in self))
 
-    def __invert__(self):
+    def __invert__(self) -> 'Mat4':
         a = self[10] * self[15] - self[11] * self[14]
         b = self[9] * self[15] - self[11] * self[13]
         c = self[9] * self[14] - self[10] * self[13]
@@ -235,14 +925,26 @@ class Mat4(tuple):
                      ndet * (self[0] * i - self[1] * n + self[2] * q),
                      pdet * (self[0] * l - self[1] * p + self[2] * r)))
 
-    def __round__(self, n=None):
-        return Mat4(tuple(round(v, n) for v in self))
+    def __round__(self, ndigits=None) -> 'Mat4':
+        return Mat4(tuple(round(v, ndigits) for v in self))
 
     def __mul__(self, other):
         raise NotImplementedError("Please use the @ operator for Matrix multiplication.")
 
-    def __matmul__(self, other):
-        assert len(other) == 16, "Can only multiply with other Mat4 types"
+    def __matmul__(self, other) -> 'Mat4':
+        assert len(other) in (4, 16), "Can only multiply with Mat4 or Vec4 types"
+
+        if type(other) is Vec4:
+            # Columns:
+            c0 = self[0::4]
+            c1 = self[1::4]
+            c2 = self[2::4]
+            c3 = self[3::4]
+            return Vec4(sum(map(_mul, c0, other)),
+                        sum(map(_mul, c1, other)),
+                        sum(map(_mul, c2, other)),
+                        sum(map(_mul, c3, other)))
+
         # Rows:
         r0 = self[0:4]
         r1 = self[4:8]
@@ -255,25 +957,29 @@ class Mat4(tuple):
         c3 = other[3::4]
 
         # Multiply and sum rows * colums:
-        return Mat4((sum(map(_operator.mul, r0, c0)),
-                     sum(map(_operator.mul, r0, c1)),
-                     sum(map(_operator.mul, r0, c2)),
-                     sum(map(_operator.mul, r0, c3)),
-
-                     sum(map(_operator.mul, r1, c0)),
-                     sum(map(_operator.mul, r1, c1)),
-                     sum(map(_operator.mul, r1, c2)),
-                     sum(map(_operator.mul, r1, c3)),
-
-                     sum(map(_operator.mul, r2, c0)),
-                     sum(map(_operator.mul, r2, c1)),
-                     sum(map(_operator.mul, r2, c2)),
-                     sum(map(_operator.mul, r2, c3)),
-
-                     sum(map(_operator.mul, r3, c0)),
-                     sum(map(_operator.mul, r3, c1)),
-                     sum(map(_operator.mul, r3, c2)),
-                     sum(map(_operator.mul, r3, c3))))
-
-    def __repr__(self):
+        return Mat4((sum(map(_mul, r0, c0)),
+                     sum(map(_mul, r0, c1)),
+                     sum(map(_mul, r0, c2)),
+                     sum(map(_mul, r0, c3)),
+
+                     sum(map(_mul, r1, c0)),
+                     sum(map(_mul, r1, c1)),
+                     sum(map(_mul, r1, c2)),
+                     sum(map(_mul, r1, c3)),
+
+                     sum(map(_mul, r2, c0)),
+                     sum(map(_mul, r2, c1)),
+                     sum(map(_mul, r2, c2)),
+                     sum(map(_mul, r2, c3)),
+
+                     sum(map(_mul, r3, c0)),
+                     sum(map(_mul, r3, c1)),
+                     sum(map(_mul, r3, c2)),
+                     sum(map(_mul, r3, c3))))
+
+    # def __getitem__(self, item):
+    #     row = [slice(0, 4), slice(4, 8), slice(8, 12), slice(12, 16)][item]
+    #     return super().__getitem__(row)
+
+    def __repr__(self) -> str:
         return f"{self.__class__.__name__}{self[0:4]}\n    {self[4:8]}\n    {self[8:12]}\n    {self[12:16]}"
diff --git a/pyglet/media/__init__.py b/pyglet/media/__init__.py
index 66ff3bc..b86a242 100644
--- a/pyglet/media/__init__.py
+++ b/pyglet/media/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -92,6 +92,8 @@ __all__ = (
     'Player',
     'PlayerGroup',
     'SourceGroup',
+    'StaticSource',
+    'StreamingSource',
     'get_encoders',
     'get_decoders',
     'add_encoders',
diff --git a/pyglet/media/buffered_logger.py b/pyglet/media/buffered_logger.py
index 9ba76ab..be90a98 100644
--- a/pyglet/media/buffered_logger.py
+++ b/pyglet/media/buffered_logger.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/codecs/__init__.py b/pyglet/media/codecs/__init__.py
index dffd5d6..8dd3f90 100644
--- a/pyglet/media/codecs/__init__.py
+++ b/pyglet/media/codecs/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -105,6 +105,12 @@ def add_default_media_codecs():
     except ImportError:
         pass
 
+    try:
+        from . import pyogg
+        add_decoders(pyogg)
+    except ImportError:
+        pass
+
 
 def have_ffmpeg():
     """Check if FFmpeg library is available.
@@ -120,7 +126,7 @@ def have_ffmpeg():
             print('FFmpeg available, using to load media files.')
         return True
 
-    except (ImportError, FileNotFoundError):
+    except (ImportError, FileNotFoundError, AttributeError):
         if _debug:
             print('FFmpeg not available.')
         return False
diff --git a/pyglet/media/codecs/base.py b/pyglet/media/codecs/base.py
index 0f32d8b..5392a3a 100644
--- a/pyglet/media/codecs/base.py
+++ b/pyglet/media/codecs/base.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -410,18 +410,6 @@ class StreamingSource(Source):
     :class:`~pyglet.media.player.Player`.
     """
 
-    @property
-    def is_queued(self):
-        """
-        bool: Determine if this source is a player current source.
-
-        Check on a :py:class:`~pyglet.media.player.Player` if this source
-        is the current source.
-
-        :deprecated: Use :attr:`is_player_source` instead.
-        """
-        return self.is_player_source
-
     def get_queue_source(self):
         """Return the ``Source`` to be used as the source for a player.
 
diff --git a/pyglet/media/codecs/ffmpeg.py b/pyglet/media/codecs/ffmpeg.py
index cef91bc..8ab3452 100644
--- a/pyglet/media/codecs/ffmpeg.py
+++ b/pyglet/media/codecs/ffmpeg.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -36,24 +36,19 @@
 """Use ffmpeg to decode audio and video media.
 """
 
-import tempfile
-from ctypes import (c_int, c_uint16, c_int32, c_int64, c_uint32, c_uint64,
-                    c_uint8, c_uint, c_double, c_float, c_ubyte, c_size_t, c_char, c_char_p,
-                    c_void_p, addressof, byref, cast, POINTER, CFUNCTYPE, Structure, Union,
-                    create_string_buffer, memmove)
 from collections import deque
+from ctypes import (c_int, c_int32, c_uint8, c_char_p,
+                    addressof, byref, cast, POINTER, Structure, create_string_buffer, memmove)
 
 import pyglet
 import pyglet.lib
-
 from pyglet import image
 from pyglet.util import asbytes, asbytes_filename, asstr
-from ..events import MediaEvent
-from ..exceptions import MediaFormatException
-from .base import StreamingSource, VideoFormat, AudioFormat
+from . import MediaDecoder
 from .base import AudioData, SourceInfo, StaticSource
+from .base import StreamingSource, VideoFormat, AudioFormat
 from .ffmpeg_lib import *
-from . import MediaEncoder, MediaDecoder
+from ..exceptions import MediaFormatException
 
 
 class FileInfo:
@@ -116,7 +111,7 @@ def ffmpeg_get_audio_buffer_size(audio_format):
 
     Buffer size can accomodate 1 sec of audio data.
     """
-    return audio_format.bytes_per_second
+    return audio_format.bytes_per_second + FF_INPUT_BUFFER_PADDING_SIZE
 
 
 def ffmpeg_init():
@@ -125,6 +120,99 @@ def ffmpeg_init():
     pass
 
 
+class MemoryFileObject:
+    """A class to manage reading and seeking of a ffmpeg file object."""
+    buffer_size = 32768
+
+    def __init__(self, file):
+        self.file = file
+        self.fmt_context = None
+        self.buffer = None
+
+        print("File object:", file)
+
+        if not getattr(self.file, 'seek', None) or not getattr(self.file, 'tell', None):
+            raise Exception("File object does not support seeking.")
+
+        # Seek to end of file to get the filesize.
+        self.file.seek(0, 2)
+        self.file_size = self.file.tell()
+        self.file.seek(0)  # Put cursor back at the beginning.
+
+        def read_data_cb(_, buff, buf_size):
+            data = self.file.read(buf_size)
+            read_size = len(data)
+            memmove(buff, data, read_size)
+            return read_size
+
+        def seek_data_cb(_, offset, whence):
+            if whence == libavformat.AVSEEK_SIZE:
+                return self.file_size
+
+            pos = self.file.seek(offset, whence)
+            return pos
+
+        self.read_func = libavformat.ffmpeg_read_func(read_data_cb)
+        self.seek_func = libavformat.ffmpeg_seek_func(seek_data_cb)
+
+    def __del__(self):
+        """These are usually freed when the source is, but no guarantee."""
+        if self.buffer:
+            try:
+                avutil.av_freep(self.buffer)
+            except OSError:
+                pass
+
+        if self.fmt_context:
+            try:
+                avutil.av_freep(self.fmt_context)
+            except OSError:
+                pass
+
+
+def ffmpeg_open_memory_file(filename, file_object):
+    """Open a media file from a file object.
+    :rtype: FFmpegFile
+    :return: The structure containing all the information for the media.
+    """
+    file = FFmpegFile()
+
+    file.context = libavformat.avformat.avformat_alloc_context()
+    file.context.contents.seekable = 1
+
+    memory_file = MemoryFileObject(file_object)
+
+    av_buf = libavutil.avutil.av_malloc(memory_file.buffer_size)
+    memory_file.buffer = cast(av_buf, c_char_p)
+
+    ptr = create_string_buffer(memory_file.buffer_size)
+
+    memory_file.fmt_context = libavformat.avformat.avio_alloc_context(
+        memory_file.buffer,
+        memory_file.buffer_size,
+        0,
+        ptr,
+        memory_file.read_func,
+        None,
+        memory_file.seek_func
+    )
+
+    file.context.contents.pb = memory_file.fmt_context
+    file.context.contents.flags |= libavformat.AVFMT_FLAG_CUSTOM_IO
+
+    result = avformat.avformat_open_input(byref(file.context), filename, None, None)
+
+    if result != 0:
+        raise FFmpegException('avformat_open_input in ffmpeg_open_filename returned an error opening file '
+                              + filename.decode("utf8")
+                              + ' Error code: ' + str(result))
+
+    result = avformat.avformat_find_stream_info(file.context, None)
+    if result < 0:
+        raise FFmpegException('Could not find stream info')
+
+    return file, memory_file
+
 def ffmpeg_open_filename(filename):
     """Open the media file.
 
@@ -136,6 +224,7 @@ def ffmpeg_open_filename(filename):
                                           filename,
                                           None,
                                           None)
+
     if result != 0:
         raise FFmpegException('avformat_open_input in ffmpeg_open_filename returned an error opening file '
                               + filename.decode("utf8")
@@ -175,15 +264,9 @@ def ffmpeg_file_info(file):
     if entry:
         info.title = asstr(entry.contents.value)
 
-    entry = avutil.av_dict_get(file.context.contents.metadata,
-                               asbytes('artist'),
-                               None,
-                               0) \
+    entry = avutil.av_dict_get(file.context.contents.metadata, asbytes('artist'), None, 0) \
             or \
-            avutil.av_dict_get(file.context.contents.metadata,
-                               asbytes('album_artist'),
-                               None,
-                               0)
+            avutil.av_dict_get(file.context.contents.metadata, asbytes('album_artist'), None, 0)
     if entry:
         info.author = asstr(entry.contents.value)
 
@@ -218,13 +301,14 @@ def ffmpeg_stream_info(file, stream_index):
     """Open the stream
     """
     av_stream = file.context.contents.streams[stream_index].contents
+
     context = av_stream.codecpar.contents
+
     if context.codec_type == AVMEDIA_TYPE_VIDEO:
         if _debug:
             print("codec_type=", context.codec_type)
             print(" codec_id=", context.codec_id)
-            codec_name = avcodec.avcodec_get_name(context.codec_id).decode('utf-8')
-            print(" codec name=", codec_name)
+            print(" codec name=", avcodec.avcodec_get_name(context.codec_id).decode('utf-8'))
             print(" codec_tag=", context.codec_tag)
             print(" extradata=", context.extradata)
             print(" extradata_size=", context.extradata_size)
@@ -286,8 +370,7 @@ def ffmpeg_stream_info(file, stream_index):
 
 def ffmpeg_open_stream(file, index):
     if not 0 <= index < file.context.contents.nb_streams:
-        raise FFmpegException('index out of range. '
-                              'Only {} streams.'.format(file.context.contents.nb_streams))
+        raise FFmpegException('index out of range. Only {} streams.'.format(file.context.contents.nb_streams))
     codec_context = avcodec.avcodec_alloc_context3(None)
     if not codec_context:
         raise MemoryError('Could not allocate Codec Context.')
@@ -323,6 +406,7 @@ def ffmpeg_open_stream(file, index):
     result = avcodec.avcodec_open2(codec_context, codec, None)
     if result < 0:
         raise FFmpegException('Could not open the media with the codec.')
+
     stream = FFmpegStream()
     stream.format_context = file.context
     stream.codec_context = codec_context
@@ -383,7 +467,7 @@ def ffmpeg_get_packet_pts(file, packet):
 
 
 def ffmpeg_get_frame_ts(stream):
-    ts = avutil.av_frame_get_best_effort_timestamp(stream.frame)
+    ts = stream.frame.contents.best_effort_timestamp
     timestamp = avutil.av_rescale_q(ts,
                                     stream.time_base,
                                     AV_TIME_BASE_Q)
@@ -438,7 +522,6 @@ class VideoPacket(_Packet):
 
     def __init__(self, packet, timestamp):
         super(VideoPacket, self).__init__(packet, timestamp)
-
         # Decoded image.  0 == not decoded yet; None == Error or discarded
         self.image = 0
         self.id = self._next_id
@@ -453,19 +536,22 @@ class FFmpegSource(StreamingSource):
     # Max increase/decrease of original sample size
     SAMPLE_CORRECTION_PERCENT_MAX = 10
 
+    # Maximum amount of packets to create for video and audio queues.
+    MAX_QUEUE_SIZE = 100
+
     def __init__(self, filename, file=None):
         self._packet = None
         self._video_stream = None
         self._audio_stream = None
+        self._stream_end = False
         self._file = None
+        self._memory_file = None
 
         if file:
-            file.seek(0)
-            self._tempfile = tempfile.NamedTemporaryFile(buffering=False)
-            self._tempfile.write(file.read())
-            filename = self._tempfile.name
+            self._file, self._memory_file = ffmpeg_open_memory_file(asbytes_filename(filename), file)
+        else:
+            self._file = ffmpeg_open_filename(asbytes_filename(filename))
 
-        self._file = ffmpeg_open_filename(asbytes_filename(filename))
         if not self._file:
             raise FFmpegException('Could not open "{0}"'.format(filename))
 
@@ -493,7 +579,6 @@ class FFmpegSource(StreamingSource):
             info = ffmpeg_stream_info(self._file, i)
 
             if isinstance(info, StreamVideoInfo) and self._video_stream is None:
-
                 stream = ffmpeg_open_stream(self._file, i)
 
                 self.video_format = VideoFormat(
@@ -509,10 +594,7 @@ class FFmpegSource(StreamingSource):
                 self._video_stream = stream
                 self._video_stream_index = i
 
-            elif (isinstance(info, StreamAudioInfo) and
-                  info.sample_bits in (8, 16) and
-                  self._audio_stream is None):
-
+            elif isinstance(info, StreamAudioInfo) and info.sample_bits in (8, 16, 24) and self._audio_stream is None:
                 stream = ffmpeg_open_stream(self._file, i)
 
                 self.audio_format = AudioFormat(
@@ -528,6 +610,7 @@ class FFmpegSource(StreamingSource):
 
                 sample_rate = stream.codec_context.contents.sample_rate
                 sample_format = stream.codec_context.contents.sample_fmt
+
                 if sample_format in (AV_SAMPLE_FMT_U8, AV_SAMPLE_FMT_U8P):
                     self.tgt_format = AV_SAMPLE_FMT_U8
                 elif sample_format in (AV_SAMPLE_FMT_S16, AV_SAMPLE_FMT_S16P):
@@ -555,14 +638,14 @@ class FFmpegSource(StreamingSource):
 
         self.audioq = deque()
         # Make queue big enough to accomodate 1.2 sec?
-        self._max_len_audioq = 50  # Need to figure out a correct amount
+        self._max_len_audioq = self.MAX_QUEUE_SIZE  # Need to figure out a correct amount
         if self.audio_format:
-            # Buffer 1 sec worth of audio
-            self._audio_buffer = \
-                (c_uint8 * ffmpeg_get_audio_buffer_size(self.audio_format))()
+             # Buffer 1 sec worth of audio
+             nbytes = ffmpeg_get_audio_buffer_size(self.audio_format)
+             self._audio_buffer = (c_uint8 * nbytes)()
 
         self.videoq = deque()
-        self._max_len_videoq = 50  # Need to figure out a correct amount
+        self._max_len_videoq = self.MAX_QUEUE_SIZE  # Need to figure out a correct amount
 
         self.start_time = self._get_start_time()
         self._duration = timestamp_from_ffmpeg(file_info.duration)
@@ -579,8 +662,6 @@ class FFmpegSource(StreamingSource):
             self.seek(0.0)
 
     def __del__(self):
-        if hasattr(self, '_tempfile'):
-            self._tempfile.close()
         if self._packet and ffmpeg_free_packet is not None:
             ffmpeg_free_packet(self._packet)
         if self._video_stream and swscale is not None:
@@ -595,11 +676,13 @@ class FFmpegSource(StreamingSource):
     def seek(self, timestamp):
         if _debug:
             print('FFmpeg seek', timestamp)
+
         ffmpeg_seek_file(
             self._file,
             timestamp_to_ffmpeg(timestamp + self.start_time)
         )
         del self._events[:]
+        self._stream_end = False
         self._clear_video_audio_queues()
         self._fillq()
 
@@ -642,14 +725,6 @@ class FFmpegSource(StreamingSource):
                 else:
                     break
 
-    def _append_audio_data(self, audio_data):
-        self.audioq.append(audio_data)
-        assert len(self.audioq) <= self._max_len_audioq
-
-    def _append_video_packet(self, video_packet):
-        self.videoq.append(video_packet)
-        assert len(self.videoq) <= self._max_len_audioq
-
     def _get_audio_packet(self):
         """Take an audio packet from the queue.
 
@@ -657,11 +732,13 @@ class FFmpegSource(StreamingSource):
         the queues if space is available. Multiple calls to this method will
         only result in one scheduled call to `_fillq`.
         """
+
         audio_data = self.audioq.popleft()
         low_lvl = self._check_low_level()
         if not low_lvl and not self._fillq_scheduled:
             pyglet.clock.schedule_once(lambda dt: self._fillq(), 0)
             self._fillq_scheduled = True
+
         return audio_data
 
     def _get_video_packet(self):
@@ -671,6 +748,8 @@ class FFmpegSource(StreamingSource):
         the queues if space is available. Multiple calls to this method will
         only result in one scheduled call to `_fillq`.
         """
+        if not self.videoq:
+            return None
         video_packet = self.videoq.popleft()
         low_lvl = self._check_low_level()
         if not low_lvl and not self._fillq_scheduled:
@@ -679,12 +758,12 @@ class FFmpegSource(StreamingSource):
         return video_packet
 
     def _clear_video_audio_queues(self):
-        "Empty both audio and video queues."
+        """Empty both audio and video queues."""
         self.audioq.clear()
         self.videoq.clear()
 
     def _fillq(self):
-        "Fill up both Audio and Video queues if space is available in both"
+        """Fill up both Audio and Video queues if space is available in both"""
         # We clear our flag.
         self._fillq_scheduled = False
         while (len(self.audioq) < self._max_len_audioq and
@@ -692,9 +771,8 @@ class FFmpegSource(StreamingSource):
             if self._get_packet():
                 self._process_packet()
             else:
+                self._stream_end = True
                 break
-                # Should maybe record that end of stream is reached in an
-                # instance member.
 
     def _check_low_level(self):
         """Check if both audio and video queues are getting very low.
@@ -731,49 +809,56 @@ class FFmpegSource(StreamingSource):
             video_packet = VideoPacket(self._packet, timestamp)
 
             if _debug:
-                print('Created and queued packet %d (%f)' % \
-                      (video_packet.id, video_packet.timestamp))
+                print('Created and queued packet %d (%f)' % (video_packet.id, video_packet.timestamp))
 
-            self._append_video_packet(video_packet)
+            self.videoq.append(video_packet)
             return video_packet
 
-        elif (self.audio_format and
-              self._packet.contents.stream_index == self._audio_stream_index):
+        elif self.audio_format and self._packet.contents.stream_index == self._audio_stream_index:
             audio_packet = AudioPacket(self._packet, timestamp)
-            self._append_audio_data(audio_packet)
+
+            self.audioq.append(audio_packet)
             return audio_packet
 
-    def get_audio_data(self, bytes, compensation_time=0.0):
-        if self.audioq:
+    def get_audio_data(self, num_bytes, compensation_time=0.0):
+        data = b''
+        timestamp = duration = 0
+
+        while len(data) < num_bytes:
+            if not self.audioq:
+                break
+
             audio_packet = self._get_audio_packet()
-            audio_data = self._decode_audio_packet(audio_packet, compensation_time)
-            audio_data_timeend = audio_data.timestamp + audio_data.duration
-        else:
-            audio_data = None
-            audio_data_timeend = None
+            buffer, timestamp, duration = self._decode_audio_packet(audio_packet, compensation_time)
 
-        if _debug:
-            print('get_audio_data')
+            if not buffer:
+                break
+            data += buffer
+
+        # No data and no audio queue left
+        if not data and not self.audioq:
+            if not self._stream_end:
+                # No more audio data in queue, but we haven't hit the stream end.
+                if _debug:
+                    print("Audio queue was starved by the audio driver.")
 
-        if audio_data is None:
-            if _debug:
-                print('No more audio data. get_audio_data returning None')
             return None
 
-        while self._events and self._events[0].timestamp <= audio_data_timeend:
+        audio_data = AudioData(data, len(data), timestamp, duration, [])
+
+        while self._events and self._events[0].timestamp <= (timestamp + duration):
             event = self._events.pop(0)
-            if event.timestamp >= audio_data.timestamp:
-                event.timestamp -= audio_data.timestamp
+            if event.timestamp >= timestamp:
+                event.timestamp -= timestamp
                 audio_data.events.append(event)
 
         if _debug:
-            print('get_audio_data returning ts {0} with events {1}'.format(
-                audio_data.timestamp, audio_data.events))
+            print('get_audio_data returning ts {0} with events {1}'.format(audio_data.timestamp, audio_data.events))
             print('remaining events are', self._events)
+
         return audio_data
 
     def _decode_audio_packet(self, audio_packet, compensation_time):
-
         while True:
             try:
                 size_out = self._ffmpeg_decode_audio(
@@ -793,88 +878,97 @@ class FFmpegSource(StreamingSource):
             duration = float(len(buffer)) / self.audio_format.bytes_per_second
             timestamp = ffmpeg_get_frame_ts(self._audio_stream)
             timestamp = timestamp_from_ffmpeg(timestamp)
-            return AudioData(buffer, len(buffer), timestamp, duration, [])
+            return buffer, timestamp, duration
 
-        return AudioData(b"", 0, 0, 0, [])
+        return None, 0, 0
 
     def _ffmpeg_decode_audio(self, packet, data_out, compensation_time):
         stream = self._audio_stream
-        data_in = packet.data
-        size_in = packet.size
+
         if stream.type != AVMEDIA_TYPE_AUDIO:
             raise FFmpegException('Trying to decode audio on a non-audio stream.')
 
-        got_frame = c_int(0)
-        bytes_used = avcodec.avcodec_decode_audio4(
+        sent_result = avcodec.avcodec_send_packet(
+            stream.codec_context,
+            packet,
+        )
+
+        if sent_result < 0:
+            buf = create_string_buffer(128)
+            avutil.av_strerror(sent_result, buf, 128)
+            descr = buf.value
+            raise FFmpegException('Error occurred sending packet to decoder. {}'.format(descr.decode()))
+
+        receive_result = avcodec.avcodec_receive_frame(
             stream.codec_context,
-            stream.frame,
-            byref(got_frame),
-            byref(packet))
-        if (bytes_used < 0):
+            stream.frame
+        )
+
+        if receive_result < 0:
             buf = create_string_buffer(128)
-            avutil.av_strerror(bytes_used, buf, 128)
+            avutil.av_strerror(receive_result, buf, 128)
             descr = buf.value
-            raise FFmpegException('Error occured while decoding audio. ' +
-                                  descr.decode())
+            raise FFmpegException('Error occurred receiving frame. {}'.format(descr.decode()))
+
         plane_size = c_int(0)
-        if got_frame:
-            data_size = avutil.av_samples_get_buffer_size(
-                byref(plane_size),
-                stream.codec_context.contents.channels,
-                stream.frame.contents.nb_samples,
-                stream.codec_context.contents.sample_fmt,
-                1)
-            if data_size < 0:
-                raise FFmpegException('Error in av_samples_get_buffer_size')
-            if len(self._audio_buffer) < data_size:
-                raise FFmpegException('Output audio buffer is too small for current audio frame!')
-
-            nb_samples = stream.frame.contents.nb_samples
-            sample_rate = stream.codec_context.contents.sample_rate
-            bytes_per_sample = avutil.av_get_bytes_per_sample(self.tgt_format)
-            channels_out = min(2, self.audio_format.channels)
-
-            wanted_nb_samples = nb_samples + compensation_time * sample_rate
-            min_nb_samples = (nb_samples * (100 - self.SAMPLE_CORRECTION_PERCENT_MAX) / 100)
-            max_nb_samples = (nb_samples * (100 + self.SAMPLE_CORRECTION_PERCENT_MAX) / 100)
-            wanted_nb_samples = min(max(wanted_nb_samples, min_nb_samples), max_nb_samples)
-            wanted_nb_samples = int(wanted_nb_samples)
-
-            if wanted_nb_samples != nb_samples:
-                res = swresample.swr_set_compensation(
-                    self.audio_convert_ctx,
-                    (wanted_nb_samples - nb_samples),
-                    wanted_nb_samples
-                )
-                if res < 0:
-                    raise FFmpegException('swr_set_compensation failed.')
-
-            data_in = stream.frame.contents.extended_data
-            p_data_out = cast(data_out, POINTER(c_uint8))
-
-            out_samples = swresample.swr_get_out_samples(self.audio_convert_ctx, nb_samples)
-            total_samples_out = swresample.swr_convert(self.audio_convert_ctx,
-                                                       byref(p_data_out), out_samples,
-                                                       data_in, nb_samples)
-            while True:
-                # We loop because there could be some more samples buffered in
-                # SwrContext. We advance the pointer where we write our samples.
-                offset = (total_samples_out * channels_out * bytes_per_sample)
-                p_data_offset = cast(
-                    addressof(p_data_out.contents) + offset,
-                    POINTER(c_uint8)
-                )
-                samples_out = swresample.swr_convert(self.audio_convert_ctx,
-                                                     byref(p_data_offset),
-                                                     out_samples - total_samples_out, None, 0)
-                if samples_out == 0:
-                    # No more samples. We can continue.
-                    break
-                total_samples_out += samples_out
 
-            size_out = (total_samples_out * channels_out * bytes_per_sample)
-        else:
-            size_out = 0
+        data_size = avutil.av_samples_get_buffer_size(
+            byref(plane_size),
+            stream.codec_context.contents.channels,
+            stream.frame.contents.nb_samples,
+            stream.codec_context.contents.sample_fmt,
+            1)
+        if data_size < 0:
+            raise FFmpegException('Error in av_samples_get_buffer_size')
+        if len(self._audio_buffer) < data_size:
+            raise FFmpegException('Output audio buffer is too small for current audio frame!')
+
+        nb_samples = stream.frame.contents.nb_samples
+        sample_rate = stream.codec_context.contents.sample_rate
+        bytes_per_sample = avutil.av_get_bytes_per_sample(self.tgt_format)
+        channels_out = min(2, self.audio_format.channels)
+
+        wanted_nb_samples = nb_samples + compensation_time * sample_rate
+        min_nb_samples = (nb_samples * (100 - self.SAMPLE_CORRECTION_PERCENT_MAX) / 100)
+        max_nb_samples = (nb_samples * (100 + self.SAMPLE_CORRECTION_PERCENT_MAX) / 100)
+        wanted_nb_samples = min(max(wanted_nb_samples, min_nb_samples), max_nb_samples)
+        wanted_nb_samples = int(wanted_nb_samples)
+
+        if wanted_nb_samples != nb_samples:
+            res = swresample.swr_set_compensation(
+                self.audio_convert_ctx,
+                (wanted_nb_samples - nb_samples),
+                wanted_nb_samples
+            )
+
+            if res < 0:
+                raise FFmpegException('swr_set_compensation failed.')
+
+        data_in = stream.frame.contents.extended_data
+        p_data_out = cast(data_out, POINTER(c_uint8))
+
+        out_samples = swresample.swr_get_out_samples(self.audio_convert_ctx, nb_samples)
+        total_samples_out = swresample.swr_convert(self.audio_convert_ctx,
+                                                   byref(p_data_out), out_samples,
+                                                   data_in, nb_samples)
+        while True:
+            # We loop because there could be some more samples buffered in
+            # SwrContext. We advance the pointer where we write our samples.
+            offset = (total_samples_out * channels_out * bytes_per_sample)
+            p_data_offset = cast(
+                addressof(p_data_out.contents) + offset,
+                POINTER(c_uint8)
+            )
+            samples_out = swresample.swr_convert(self.audio_convert_ctx,
+                                                 byref(p_data_offset),
+                                                 out_samples - total_samples_out, None, 0)
+            if samples_out == 0:
+                # No more samples. We can continue.
+                break
+            total_samples_out += samples_out
+
+        size_out = (total_samples_out * channels_out * bytes_per_sample)
+
         return size_out
 
     def _decode_video_packet(self, video_packet):
@@ -887,7 +981,9 @@ class FFmpegSource(StreamingSource):
         width = self.video_format.width
         height = self.video_format.height
         pitch = width * 4
-        buffer = (c_uint8 * (pitch * height))()
+        # https://ffmpeg.org/doxygen/3.3/group__lavc__decoding.html#ga8f5b632a03ce83ac8e025894b1fc307a
+        nbytes = (pitch * height + FF_INPUT_BUFFER_PADDING_SIZE)
+        buffer = (c_uint8 * nbytes)()
         try:
             result = self._ffmpeg_decode_video(video_packet.packet,
                                                buffer)
@@ -902,7 +998,7 @@ class FFmpegSource(StreamingSource):
         video_packet.image = image_data
 
         if _debug:
-            print('Decoding video packet at timestamp', video_packet.timestamp)
+            print('Decoding video packet at timestamp', video_packet, video_packet.timestamp)
 
             # t2 = clock.time()
             # pr.disable()
@@ -921,16 +1017,27 @@ class FFmpegSource(StreamingSource):
         if stream.type != AVMEDIA_TYPE_VIDEO:
             raise FFmpegException('Trying to decode video on a non-video stream.')
 
-        got_picture = c_int(0)
-        bytes_used = avcodec.avcodec_decode_video2(
+        sent_result = avcodec.avcodec_send_packet(
             stream.codec_context,
-            stream.frame,
-            byref(got_picture),
-            byref(packet))
-        if bytes_used < 0:
-            raise FFmpegException('Error decoding a video packet.')
-        if not got_picture:
-            raise FFmpegException('No frame could be decompressed')
+            packet,
+        )
+
+        if sent_result < 0:
+            buf = create_string_buffer(128)
+            avutil.av_strerror(sent_result, buf, 128)
+            descr = buf.value
+            raise FFmpegException('Video: Error occurred sending packet to decoder. {}'.format(descr.decode()))
+
+        receive_result = avcodec.avcodec_receive_frame(
+            stream.codec_context,
+            stream.frame
+        )
+
+        if receive_result < 0:
+            buf = create_string_buffer(128)
+            avutil.av_strerror(receive_result, buf, 128)
+            descr = buf.value
+            raise FFmpegException('Video: Error occurred receiving frame. {}'.format(descr.decode()))
 
         avutil.av_image_fill_arrays(rgba_ptrs, rgba_stride, data_out,
                                     AV_PIX_FMT_RGBA, width, height, 1)
@@ -949,24 +1056,29 @@ class FFmpegSource(StreamingSource):
                           height,
                           rgba_ptrs,
                           rgba_stride)
-        return bytes_used
+        return receive_result
 
     def get_next_video_timestamp(self):
         if not self.video_format:
             return
 
+        ts = None
+
         if self.videoq:
             while True:
                 # We skip video packets which are not video frames
                 # This happens in mkv files for the first few frames.
-                video_packet = self.videoq[0]
+                try:
+                    video_packet = self.videoq.popleft()
+                except IndexError:
+                    break
                 if video_packet.image == 0:
                     self._decode_video_packet(video_packet)
                 if video_packet.image is not None:
+                    ts = video_packet.timestamp
+                    self.videoq.appendleft(video_packet)  # put it back
                     break
                 self._get_video_packet()
-
-            ts = video_packet.timestamp
         else:
             ts = None
 
@@ -982,6 +1094,8 @@ class FFmpegSource(StreamingSource):
             # We skip video packets which are not video frames
             # This happens in mkv files for the first few frames.
             video_packet = self._get_video_packet()
+            if not video_packet:
+                return None
             if video_packet.image == 0:
                 self._decode_video_packet(video_packet)
             if video_packet.image is not None or not skip_empty_frame:
diff --git a/pyglet/media/codecs/ffmpeg_lib/__init__.py b/pyglet/media/codecs/ffmpeg_lib/__init__.py
index 7966ba2..4b3eb21 100644
--- a/pyglet/media/codecs/ffmpeg_lib/__init__.py
+++ b/pyglet/media/codecs/ffmpeg_lib/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -38,3 +38,5 @@ from .libavutil import *
 from .libavformat import *
 from .libswresample import *
 from .libswscale import *
+from .compat import *
+apply_version_changes()
diff --git a/pyglet/media/codecs/ffmpeg_lib/compat.py b/pyglet/media/codecs/ffmpeg_lib/compat.py
new file mode 100644
index 0000000..375f425
--- /dev/null
+++ b/pyglet/media/codecs/ffmpeg_lib/compat.py
@@ -0,0 +1,98 @@
+# ----------------------------------------------------------------------------
+# pyglet
+# Copyright (c) 2006-2008 Alex Holkner
+# Copyright (c) 2008-2022 pyglet contributors
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+#  * Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+#  * Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in
+#    the documentation and/or other materials provided with the
+#    distribution.
+#  * Neither the name of pyglet nor the names of its
+#    contributors may be used to endorse or promote products
+#    derived from this software without specific prior written
+#    permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# ----------------------------------------------------------------------------
+
+from collections import namedtuple
+
+CustomField = namedtuple("CustomField", "fields removals")
+
+# Versions of the loaded libraries
+versions = {
+    'avcodec': 0,
+    'avformat': 0,
+    'avutil': 0,
+    'swresample': 0,
+    'swscale': 0,
+}
+
+# Group codecs by version they are usually packaged with.
+release_versions = [
+    {'avcodec': 58, 'avformat': 58, 'avutil': 56, 'swresample': 3, 'swscale': 5},
+    {'libavcodec': 59, 'avformat': 59, 'avutil': 57, 'swresample': 4, 'swscale': 6}
+]
+
+# Removals done per library and version.
+_version_changes = {
+    'avcodec': {},
+    'avformat': {},
+    'avutil': {},
+    'swresample': {},
+    'swscale': {}
+}
+
+
+def set_version(library, version):
+    versions[library] = version
+
+
+def add_version_changes(library, version, structure, fields, removals):
+    if version not in _version_changes[library]:
+        _version_changes[library][version] = {}
+
+    if structure in _version_changes[library][version]:
+        raise Exception("Structure: {} from: {} has already been added for version {}.".format(structure,
+                                                                                               library,
+                                                                                               version)
+                        )
+
+    _version_changes[library][version][structure] = CustomField(fields, removals)
+
+
+def apply_version_changes():
+    """Apply version changes to Structures in FFmpeg libraries.
+       Field data can vary from version to version, however assigning _fields_ automatically assigns memory.
+       _fields_ can also not be re-assigned. Use a temporary list that can be manipulated before setting the
+       _fields_ of the Structure."""
+
+    for library, data in _version_changes.items():
+        for version in data:
+            for structure, cf_data in _version_changes[library][version].items():
+                if versions[library] == version:
+                    if cf_data.removals:
+                        for remove_field in cf_data.removals:
+                            for field in list(cf_data.fields):
+                                if field[0] == remove_field:
+                                    cf_data.fields.remove(field)
+
+                    structure._fields_ = cf_data.fields
diff --git a/pyglet/media/codecs/ffmpeg_lib/libavcodec.py b/pyglet/media/codecs/ffmpeg_lib/libavcodec.py
index a6f5211..222726b 100644
--- a/pyglet/media/codecs/ffmpeg_lib/libavcodec.py
+++ b/pyglet/media/codecs/ffmpeg_lib/libavcodec.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -36,21 +36,28 @@
 """Wrapper for include/libavcodec/avcodec.h
 """
 
-from ctypes import c_int, c_uint16, c_int32, c_int64, c_uint32, c_uint64
-from ctypes import c_uint8, c_uint, c_double, c_float, c_ubyte, c_size_t, c_char, c_char_p
-from ctypes import c_void_p, addressof, byref, cast, POINTER, CFUNCTYPE, Structure, Union
-from ctypes import create_string_buffer, memmove
+from ctypes import c_int, c_uint16, c_int64, c_uint32, c_uint64
+from ctypes import c_uint8, c_uint, c_float, c_char_p
+from ctypes import c_void_p, POINTER, CFUNCTYPE, Structure
 
-import pyglet
 import pyglet.lib
+from pyglet.util import debug_print
+from . import compat
 from . import libavutil
 
+_debug = debug_print('debug_media')
+
 avcodec = pyglet.lib.load_library(
     'avcodec',
-    win32='avcodec-58',
-    darwin='avcodec.58'
+    win32=('avcodec-59', 'avcodec-58'),
+    darwin=('avcodec.59', 'avcodec.58')
 )
 
+avcodec.avcodec_version.restype = c_int
+
+compat.set_version('avcodec', avcodec.avcodec_version() >> 16)
+
+
 FF_INPUT_BUFFER_PADDING_SIZE = 32
 
 
@@ -64,9 +71,13 @@ class AVPacketSideData(Structure):
 
 AVBufferRef = libavutil.AVBufferRef
 
+AVRational = libavutil.AVRational
+
 
 class AVPacket(Structure):
-    _fields_ = [
+    pass
+
+AVPacket_Fields = [
         ('buf', POINTER(AVBufferRef)),
         ('pts', c_int64),
         ('dts', c_int64),
@@ -78,15 +89,22 @@ class AVPacket(Structure):
         ('side_data_elems', c_int),
         ('duration', c_int64),
         ('pos', c_int64),
-        ('convergence_duration', c_int64)  # Deprecated
+        ('opaque', c_void_p),  # 5.x only
+        ('opaque_ref', c_void_p),  # 5.x only
+        ('time_base', AVRational),  # 5.x only
+        ('convergence_duration', c_int64)  # 4.x only
     ]
 
 
-class AVCodecParserContext(Structure):
-    pass
+compat.add_version_changes('avcodec', 58, AVPacket, AVPacket_Fields,
+                           removals=('opaque', 'opaque_ref', 'time_base'))
 
+compat.add_version_changes('avcodec', 59, AVPacket, AVPacket_Fields,
+                           removals=('convergence_duration',))
 
-AVRational = libavutil.AVRational
+
+class AVCodecParserContext(Structure):
+    pass
 
 
 class AVCodecParameters(Structure):
@@ -112,14 +130,14 @@ class AVCodecParameters(Structure):
         ('color_space', c_int),
         ('chroma_location', c_int),
         ('video_delay', c_int),
-        ('channel_layout', c_uint64),
-        ('channels', c_int),
+        ('channel_layout', c_uint64),  # deprecated as of 59
+        ('channels', c_int),  # deprecated as of 59
         ('sample_rate', c_int),
         ('block_align', c_int),
         ('frame_size', c_int),
         ('initial_padding', c_int),
         ('trailing_padding', c_int),
-        ('seek_preroll', c_int),
+        ('seek_preroll', c_int)
     ]
 
 
@@ -178,7 +196,8 @@ class AVHWAccel(Structure):
 AVClass = libavutil.AVClass
 AVFrame = libavutil.AVFrame
 AV_NUM_DATA_POINTERS = libavutil.AV_NUM_DATA_POINTERS
-AVCodecContext._fields_ = [
+
+AVCodecContext_Fields = [
     ('av_class', POINTER(AVClass)),
     ('log_level_offset', c_int),
     ('codec_type', c_int),
@@ -211,10 +230,10 @@ AVCodecContext._fields_ = [
     ('get_format', CFUNCTYPE(c_int, POINTER(AVCodecContext), POINTER(c_int))),
     ('max_b_frames', c_int),
     ('b_quant_factor', c_float),
-    ('b_frame_strategy', c_int),  # Deprecated
+    ('b_frame_strategy', c_int),  # Deprecated. Removed in 59.
     ('b_quant_offset', c_float),
     ('has_b_frames', c_int),
-    ('mpeg_quant', c_int),  # Deprecated
+    ('mpeg_quant', c_int),  # Deprecated. Removed in 59.
     ('i_quant_factor', c_float),
     ('i_quant_offset', c_float),
     ('lumi_masking', c_float),
@@ -223,7 +242,7 @@ AVCodecContext._fields_ = [
     ('p_masking', c_float),
     ('dark_masking', c_float),
     ('slice_count', c_int),
-    ('prediction_method', c_int),  # Deprecated
+    ('prediction_method', c_int),  # Deprecated. Removed in 59.
     ('slice_offset', POINTER(c_int)),
     ('sample_aspect_ratio', AVRational),
     ('me_cmp', c_int),
@@ -232,7 +251,7 @@ AVCodecContext._fields_ = [
     ('ildct_cmp', c_int),
     ('dia_size', c_int),
     ('last_predictor_count', c_int),
-    ('pre_me', c_int),  # Deprecated
+    ('pre_me', c_int),  # Deprecated. Removed in 59.
     ('me_pre_cmp', c_int),
     ('pre_dia_size', c_int),
     ('me_subpel_quality', c_int),
@@ -241,21 +260,21 @@ AVCodecContext._fields_ = [
     ('mb_decision', c_int),
     ('intra_matrix', POINTER(c_uint16)),
     ('inter_matrix', POINTER(c_uint16)),
-    ('scenechange_threshold', c_int),  # Deprecated
-    ('noise_reduction', c_int),  # Deprecated
+    ('scenechange_threshold', c_int),  # Deprecated. Removed in 59.
+    ('noise_reduction', c_int),  # Deprecated. Removed in 59.
     ('intra_dc_precision', c_int),
     ('skip_top', c_int),
     ('skip_bottom', c_int),
     ('mb_lmin', c_int),
     ('mb_lmax', c_int),
-    ('me_penalty_compensation', c_int),  # Deprecated
+    ('me_penalty_compensation', c_int),  # Deprecated. Removed in 59.
     ('bidir_refine', c_int),
-    ('brd_scale', c_int),  # Deprecated
+    ('brd_scale', c_int),  # Deprecated. Removed in 59.
     ('keyint_min', c_int),
     ('refs', c_int),
-    ('chromaoffset', c_int),  # Deprecated
+    ('chromaoffset', c_int),  # Deprecated. Removed in 59.
     ('mv0_threshold', c_int),
-    ('b_sensitivity', c_int),  # Deprecated
+    ('b_sensitivity', c_int),  # Deprecated. Removed in 59.
     ('color_primaries', c_int),
     ('color_trc', c_int),
     ('colorspace', c_int),
@@ -275,7 +294,7 @@ AVCodecContext._fields_ = [
     ('audio_service_type', c_int),
     ('request_sample_fmt', c_int),
     ('get_buffer2', CFUNCTYPE(c_int, POINTER(AVCodecContext), POINTER(AVFrame), c_int)),
-    ('refcounted_frames', c_int),  # Deprecated
+    ('refcounted_frames', c_int),  # Deprecated. Removed in 59.
     ('qcompress', c_float),
     ('qblur', c_float),
     ('qmin', c_int),
@@ -289,28 +308,28 @@ AVCodecContext._fields_ = [
     ('rc_max_available_vbv_use', c_float),
     ('rc_min_vbv_overflow_use', c_float),
     ('rc_initial_buffer_occupancy', c_int),
-    ('coder_type', c_int),  # Deprecated
-    ('context_model', c_int),  # Deprecated
-    ('frame_skip_threshold', c_int),  # Deprecated
-    ('frame_skip_factor', c_int),  # Deprecated
-    ('frame_skip_exp', c_int),  # Deprecated
-    ('frame_skip_cmp', c_int),  # Deprecated
+    ('coder_type', c_int),  # Deprecated. Removed in 59.
+    ('context_model', c_int),  # Deprecated. Removed in 59.
+    ('frame_skip_threshold', c_int),  # Deprecated. Removed in 59.
+    ('frame_skip_factor', c_int),  # Deprecated. Removed in 59.
+    ('frame_skip_exp', c_int),  # Deprecated. Removed in 59.
+    ('frame_skip_cmp', c_int),  # Deprecated. Removed in 59.
     ('trellis', c_int),
-    ('min_prediction_order', c_int),  # Deprecated
-    ('max_prediction_order', c_int),  # Deprecated
-    ('timecode_frame_start', c_int64),  # Deprecated
-    ('rtp_callback', CFUNCTYPE(None,  # Deprecated
-                               POINTER(AVCodecContext), c_void_p, c_int, c_int)),
-    ('rtp_payload_size', c_int),  # Deprecated
-    ('mv_bits', c_int),  # Deprecated
-    ('header_bits', c_int),  # Deprecated
-    ('i_tex_bits', c_int),  # Deprecated
-    ('p_tex_bits', c_int),  # Deprecated
-    ('i_count', c_int),  # Deprecated
-    ('p_count', c_int),  # Deprecated
-    ('skip_count', c_int),  # Deprecated
-    ('misc_bits', c_int),  # Deprecated
-    ('frame_bits', c_int),  # Deprecated
+    ('min_prediction_order', c_int),  # Deprecated. Removed in 59.
+    ('max_prediction_order', c_int),  # Deprecated. Removed in 59.
+    ('timecode_frame_start', c_int64),  # Deprecated. Removed in 59.
+    ('rtp_callback', CFUNCTYPE(None,  # Deprecated. Removed in 59.
+                              POINTER(AVCodecContext), c_void_p, c_int, c_int)),
+    ('rtp_payload_size', c_int),  # Deprecated. Removed in 59.
+    ('mv_bits', c_int),  # Deprecated. Removed in 59.
+    ('header_bits', c_int),  # Deprecated. Removed in 59.
+    ('i_tex_bits', c_int),  # Deprecated. Removed in 59.
+    ('p_tex_bits', c_int),  # Deprecated. Removed in 59.
+    ('i_count', c_int),  # Deprecated. Removed in 59.
+    ('p_count', c_int),  # Deprecated. Removed in 59.
+    ('skip_count', c_int),  # Deprecated. Removed in 59.
+    ('misc_bits', c_int),  # Deprecated. Removed in 59.
+    ('frame_bits', c_int),  # Deprecated. Removed in 59.
     ('stats_out', c_char_p),
     ('stats_in', c_char_p),
     ('workaround_bugs', c_int),
@@ -327,7 +346,7 @@ AVCodecContext._fields_ = [
     ('bits_per_coded_sample', c_int),
     ('bits_per_raw_sample', c_int),
     ('lowres', c_int),
-    ('coded_frame', POINTER(AVFrame)),  # Deprecated
+    ('coded_frame', POINTER(AVFrame)),  # Deprecated. Removed in 59.
     ('thread_count', c_int),
     ('thread_type', c_int),
     ('active_thread_type', c_int),
@@ -348,8 +367,8 @@ AVCodecContext._fields_ = [
     ('skip_frame', c_int),
     ('subtitle_header', POINTER(c_uint8)),
     ('subtitle_header_size', c_int),
-    ('vbv_delay', c_uint64),  # Deprecated
-    ('side_data_only_packets', c_int),  # Deprecated
+    ('vbv_delay', c_uint64),  # Deprecated. Removed in 59.
+    ('side_data_only_packets', c_int),  # Deprecated. Removed in 59.
     ('initial_padding', c_int),
     ('framerate', AVRational),
     # !
@@ -379,9 +398,20 @@ AVCodecContext._fields_ = [
     ('hwaccel_flags', c_int),
     ('apply_cropping', c_int),
     ('extra_hw_frames', c_int)
-
 ]
 
+compat.add_version_changes('avcodec', 58, AVCodecContext, AVCodecContext_Fields, removals=None)
+
+compat.add_version_changes('avcodec', 59, AVCodecContext, AVCodecContext_Fields,
+    removals=('b_frame_strategy', 'mpeg_quant', 'prediction_method', 'pre_me', 'scenechange_threshold',
+                  'noise_reduction', 'me_penalty_compensation', 'brd_scale', 'chromaoffset', 'b_sensitivity',
+                  'refcounted_frames', 'coder_type', 'context_model', 'coder_type', 'context_model',
+                  'frame_skip_threshold', 'frame_skip_factor', 'frame_skip_exp', 'frame_skip_cmp',
+                  'min_prediction_order', 'max_prediction_order', 'timecode_frame_start', 'rtp_callback',
+                  'rtp_payload_size', 'mv_bits', 'header_bits', 'i_tex_bits', 'p_tex_bits', 'i_count', 'p_count',
+                  'skip_count', 'misc_bits', 'frames_bits', 'coded_frame', 'vbv_delay', 'side_data_only_packets')
+)
+
 AV_CODEC_ID_VP8 = 139
 AV_CODEC_ID_VP9 = 167
 
@@ -400,14 +430,13 @@ avcodec.avcodec_open2.argtypes = [POINTER(AVCodecContext),
 avcodec.avcodec_free_context.argtypes = [POINTER(POINTER(AVCodecContext))]
 avcodec.av_packet_alloc.restype = POINTER(AVPacket)
 avcodec.av_init_packet.argtypes = [POINTER(AVPacket)]
-avcodec.avcodec_decode_audio4.restype = c_int
-avcodec.avcodec_decode_audio4.argtypes = [POINTER(AVCodecContext),
-                                          POINTER(AVFrame), POINTER(c_int),
-                                          POINTER(AVPacket)]
-avcodec.avcodec_decode_video2.restype = c_int
-avcodec.avcodec_decode_video2.argtypes = [POINTER(AVCodecContext),
-                                          POINTER(AVFrame), POINTER(c_int),
-                                          POINTER(AVPacket)]
+
+avcodec.avcodec_receive_frame.restype = c_int
+avcodec.avcodec_receive_frame.argtypes = [POINTER(AVCodecContext), POINTER(AVFrame)]
+
+avcodec.avcodec_send_packet.restype = c_int
+avcodec.avcodec_send_packet.argtypes = [POINTER(AVCodecContext), POINTER(AVPacket)]
+
 avcodec.avcodec_flush_buffers.argtypes = [POINTER(AVCodecContext)]
 avcodec.avcodec_alloc_context3.restype = POINTER(AVCodecContext)
 avcodec.avcodec_alloc_context3.argtypes = [POINTER(AVCodec)]
diff --git a/pyglet/media/codecs/ffmpeg_lib/libavformat.py b/pyglet/media/codecs/ffmpeg_lib/libavformat.py
index d248244..82f5bf0 100644
--- a/pyglet/media/codecs/ffmpeg_lib/libavformat.py
+++ b/pyglet/media/codecs/ffmpeg_lib/libavformat.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -34,31 +34,40 @@
 # ----------------------------------------------------------------------------
 """Wrapper for include/libavformat/avformat.h
 """
-from ctypes import c_int, c_uint16, c_int32, c_int64, c_uint32, c_uint64
-from ctypes import c_uint8, c_uint, c_double, c_float, c_ubyte, c_size_t, c_char, c_char_p
-from ctypes import c_void_p, addressof, byref, cast, POINTER, CFUNCTYPE, Structure, Union
-from ctypes import create_string_buffer, memmove
+from ctypes import c_int, c_int64
+from ctypes import c_uint8, c_uint, c_double, c_ubyte, c_size_t, c_char, c_char_p
+from ctypes import c_void_p, POINTER, CFUNCTYPE, Structure
 
-import pyglet
 import pyglet.lib
+from pyglet.util import debug_print
+from . import compat
 from . import libavcodec
 from . import libavutil
 
+_debug = debug_print('debug_media')
+
 avformat = pyglet.lib.load_library(
     'avformat',
-    win32='avformat-58',
-    darwin='avformat.58'
+    win32=('avformat-59', 'avformat-58'),
+    darwin=('avformat.59', 'avformat.58')
 )
 
+avformat.avformat_version.restype = c_int
+
+compat.set_version('avformat', avformat.avformat_version() >> 16)
+
 AVSEEK_FLAG_BACKWARD = 1  # ///< seek backward
 AVSEEK_FLAG_BYTE = 2  # ///< seeking based on position in bytes
 AVSEEK_FLAG_ANY = 4  # ///< seek to any frame, even non-keyframes
 AVSEEK_FLAG_FRAME = 8  # ///< seeking based on frame number
+AVSEEK_SIZE = 0x10000
+AVFMT_FLAG_CUSTOM_IO = 0x0080
 
 MAX_REORDER_DELAY = 16
 
 
-class AVPacketList(Structure): pass
+class AVPacketList(Structure):
+    pass
 
 
 class AVInputFormat(Structure):
@@ -131,13 +140,18 @@ AVCodecParameters = libavcodec.AVCodecParameters
 AVRational = libavutil.AVRational
 AVDictionary = libavutil.AVDictionary
 AVFrame = libavutil.AVFrame
+AVClass = libavutil.AVClass
+AVCodec = libavcodec.AVCodec
+
 
 
 class AVStream(Structure):
-    _fields_ = [
+    pass
+
+AVStream_Fields = [
         ('index', c_int),
         ('id', c_int),
-        ('codec', POINTER(AVCodecContext)),
+        ('codec', POINTER(AVCodecContext)),  # Deprecated. Removed in 59.
         ('priv_data', c_void_p),
         ('time_base', AVRational),
         ('start_time', c_int64),
@@ -153,48 +167,17 @@ class AVStream(Structure):
         ('nb_side_data', c_int),
         ('event_flags', c_int),
         ('r_frame_rate', AVRational),
-        ('recommended_encoder_configuration', c_char_p),
+        ('recommended_encoder_configuration', c_char_p),  # Deprecated. Removed in 59.
         ('codecpar', POINTER(AVCodecParameters)),
-        ('info', POINTER(AVStreamInfo)),
+        ('info', POINTER(AVStreamInfo)),  # Deprecated. Removed in 59.
         ('pts_wrap_bits', c_int),
-        ('first_dts', c_int64),
-        ('cur_dts', c_int64),
-        ('last_IP_pts', c_int64),
-        ('last_IP_duration', c_int),
-        ('probe_packets', c_int),
-        ('codec_info_nb_frames', c_int),
-        ('need_parsing', c_int),
-        ('parser', POINTER(AVCodecParserContext)),
-        ('last_in_packet_buffer', POINTER(AVPacketList)),
-        ('probe_data', AVProbeData),
-        ('pts_buffer', c_int64 * (MAX_REORDER_DELAY + 1)),
-        ('index_entries', POINTER(AVIndexEntry)),
-        ('nb_index_entries', c_int),
-        ('index_entries_allocated_size', c_uint),
-        ('stream_identifier', c_int),
-        ('interleaver_chunk_size', c_int64),
-        ('interleaver_chunk_duration', c_int64),
-        ('request_probe', c_int),
-        ('skip_to_keyframe', c_int),
-        ('skip_samples', c_int),
-        ('start_skip_samples', c_int64),
-        ('first_discard_sample', c_int64),
-        ('last_discard_sample', c_int64),
-        ('nb_decoded_frames', c_int),
-        ('mux_ts_offset', c_int64),
-        ('pts_wrap_reference', c_int64),
-        ('pts_wrap_behavior', c_int),
-        ('update_initial_durations_done', c_int),
-        ('pts_reorder_error', c_int64 * (MAX_REORDER_DELAY + 1)),
-        ('pts_reorder_error_count', c_uint8 * (MAX_REORDER_DELAY + 1)),
-        ('last_dts_for_order_check', c_int64),
-        ('dts_ordered', c_uint8),
-        ('dts_misordered', c_uint8),
-        ('inject_global_side_data', c_int),
-        ('display_aspect_ratio', AVRational),
-        ('internal', POINTER(AVStreamInternal))
     ]
 
+compat.add_version_changes('avformat', 58, AVStream, AVStream_Fields, removals=None)
+
+compat.add_version_changes('avformat', 59, AVStream, AVStream_Fields,
+                           removals=('codec', 'recommended_encoder_configuration', 'info'))
+
 
 class AVProgram(Structure):
     pass
@@ -215,15 +198,11 @@ class AVIOInterruptCB(Structure):
     ]
 
 
-AVClass = libavutil.AVClass
-AVCodec = libavcodec.AVCodec
-
-
 class AVFormatContext(Structure):
     pass
 
 
-AVFormatContext._fields_ = [
+AVFormatContext_Fields = [
     ('av_class', POINTER(AVClass)),
     ('iformat', POINTER(AVInputFormat)),
     ('oformat', POINTER(AVOutputFormat)),
@@ -232,7 +211,7 @@ AVFormatContext._fields_ = [
     ('ctx_flags', c_int),
     ('nb_streams', c_uint),
     ('streams', POINTER(POINTER(AVStream))),
-    ('filename', c_char * 1024),  # Deprecated
+    ('filename', c_char * 1024),  # Deprecated. Removed in 59
     ('url', c_char_p),
     ('start_time', c_int64),
     ('duration', c_int64),
@@ -279,7 +258,7 @@ AVFormatContext._fields_ = [
     ('format_probesize', c_int),
     ('codec_whitelist', c_char_p),
     ('format_whitelist', c_char_p),
-    ('internal', POINTER(AVFormatInternal)),
+    ('internal', POINTER(AVFormatInternal)),  # Deprecated. Removed in 59
     ('io_repositioned', c_int),
     ('video_codec', POINTER(AVCodec)),
     ('audio_codec', POINTER(AVCodec)),
@@ -293,7 +272,6 @@ AVFormatContext._fields_ = [
     ('output_ts_offset', c_int64),
     ('dump_separator', POINTER(c_uint8)),
     ('data_codec_id', c_int),
-    # ! one more in here?
     ('protocol_whitelist', c_char_p),
     ('io_open', CFUNCTYPE(c_int,
                           POINTER(AVFormatContext),
@@ -303,10 +281,19 @@ AVFormatContext._fields_ = [
     ('io_close', CFUNCTYPE(None,
                            POINTER(AVFormatContext), POINTER(AVIOContext))),
     ('protocol_blacklist', c_char_p),
-    ('max_streams', c_int)
+    ('max_streams', c_int),
+    ('skip_estimate_duration_from_pts', c_int),  # Added in 59.
+    ('max_probe_packets', c_int), # Added in 59.
+    ('io_close2', CFUNCTYPE(c_int, POINTER(AVFormatContext), POINTER(AVIOContext))) # Added in 59.
 ]
 
-avformat.av_register_all.restype = None
+compat.add_version_changes('avformat', 58, AVFormatContext, AVFormatContext_Fields,
+                           removals=('skip_estimate_duration_from_pts', 'max_probe_packets', 'io_close2'))
+
+compat.add_version_changes('avformat', 59, AVFormatContext, AVFormatContext_Fields,
+                           removals=('filename', 'internal'))
+
+
 avformat.av_find_input_format.restype = c_int
 avformat.av_find_input_format.argtypes = [c_int]
 avformat.avformat_open_input.restype = c_int
@@ -335,6 +322,19 @@ avformat.av_guess_frame_rate.restype = AVRational
 avformat.av_guess_frame_rate.argtypes = [POINTER(AVFormatContext),
                                          POINTER(AVStream), POINTER(AVFrame)]
 
+ffmpeg_read_func = CFUNCTYPE(c_int, c_void_p, POINTER(c_char), c_int)
+ffmpeg_write_func = CFUNCTYPE(c_int, c_void_p, POINTER(c_char), c_int)
+ffmpeg_seek_func = CFUNCTYPE(c_int64, c_void_p, c_int64, c_int)
+
+avformat.avio_alloc_context.restype = POINTER(AVIOContext)
+avformat.avio_alloc_context.argtypes = [c_char_p, c_int, c_int, c_void_p, ffmpeg_read_func, c_void_p, ffmpeg_seek_func]
+
+avformat.avformat_alloc_context.restype = POINTER(AVFormatContext)
+avformat.avformat_alloc_context.argtypes = []
+
+avformat.avformat_free_context.restype = c_void_p
+avformat.avformat_free_context.argtypes = [POINTER(AVFormatContext)]
+
 __all__ = [
     'avformat',
     'AVSEEK_FLAG_BACKWARD',
diff --git a/pyglet/media/codecs/ffmpeg_lib/libavutil.py b/pyglet/media/codecs/ffmpeg_lib/libavutil.py
index 49a62ce..77e2e7c 100644
--- a/pyglet/media/codecs/ffmpeg_lib/libavutil.py
+++ b/pyglet/media/codecs/ffmpeg_lib/libavutil.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -34,20 +34,25 @@
 # ----------------------------------------------------------------------------
 """Wrapper for include/libavutil/avutil.h
 """
-from ctypes import c_int, c_uint16, c_int32, c_int64, c_uint32, c_uint64
-from ctypes import c_uint8, c_int8, c_uint, c_double, c_float, c_ubyte, c_size_t, c_char
-from ctypes import c_char_p, c_void_p, addressof, byref, cast, POINTER, CFUNCTYPE, Structure
-from ctypes import Union, create_string_buffer, memmove
+from ctypes import c_char_p, c_void_p, POINTER, Structure
+from ctypes import c_int, c_int64, c_uint64
+from ctypes import c_uint8, c_int8, c_uint, c_size_t
 
-import pyglet
 import pyglet.lib
+from pyglet.util import debug_print
+from . import compat
+
+_debug = debug_print('debug_media')
 
 avutil = pyglet.lib.load_library(
     'avutil',
-    win32='avutil-56',
-    darwin='avutil.56'
+    win32=('avutil-57', 'avutil-56'),
+    darwin=('avutil.57', 'avutil-56')
 )
 
+avutil.avutil_version.restype = c_int
+compat.set_version('avutil', avutil.avutil_version() >> 16)
+
 AVMEDIA_TYPE_UNKNOWN = -1
 AVMEDIA_TYPE_VIDEO = 0
 AVMEDIA_TYPE_AUDIO = 1
@@ -75,7 +80,13 @@ AV_PIX_FMT_RGB24 = 2
 AV_PIX_FMT_ARGB = 25
 AV_PIX_FMT_RGBA = 26
 
-
+AVChannelOrder = c_int
+class AVChannelLayout(Structure):
+    _fields_ = [
+        ('order', c_int),
+        ('nb_channels', c_int),
+        # .. more
+    ]
 class AVBuffer(Structure):
     _fields_ = [
         ('data', POINTER(c_uint8)),
@@ -116,70 +127,81 @@ class AVRational(Structure):
         ('den', c_int)
     ]
 
+    def __repr__(self):
+        return f"AVRational({self.num}/{self.den})"
+
 
 class AVFrameSideData(Structure):
     pass
 
 
 class AVFrame(Structure):
-    _fields_ = [
-        ('data', POINTER(c_uint8) * AV_NUM_DATA_POINTERS),
-        ('linesize', c_int * AV_NUM_DATA_POINTERS),
-        ('extended_data', POINTER(POINTER(c_uint8))),
-        ('width', c_int),
-        ('height', c_int),
-        ('nb_samples', c_int),
-        ('format', c_int),
-        ('key_frame', c_int),
-        ('pict_type', c_int),
-        ('sample_aspect_ratio', AVRational),
-        ('pts', c_int64),
-        ('pkt_pts', c_int64),  # Deprecated
-        ('pkt_dts', c_int64),
-        ('coded_picture_number', c_int),
-        ('display_picture_number', c_int),
-        ('quality', c_int),
-        ('opaque', c_void_p),
-        ('error', c_uint64 * AV_NUM_DATA_POINTERS),  # Deprecated
-        ('repeat_pict', c_int),
-        ('interlaced_frame', c_int),
-        ('top_field_first', c_int),
-        ('palette_has_changed', c_int),
-        ('reordered_opaque', c_int64),
-        ('sample_rate', c_int),
-        ('channel_layout', c_uint64),
-        ('buf', POINTER(AVBufferRef) * AV_NUM_DATA_POINTERS),
-        ('extended_buf', POINTER(POINTER(AVBufferRef))),
-        ('nb_extended_buf', c_int),
-        ('side_data', POINTER(POINTER(AVFrameSideData))),
-        ('nb_side_data', c_int),
-        ('flags', c_int),
-        ('color_range', c_int),
-        ('color_primaries', c_int),
-        ('color_trc', c_int),
-        ('colorspace', c_int),
-        ('chroma_location', c_int),
-        ('best_effort_timestamp', c_int64),
-        ('pkt_pos', c_int64),
-        ('pkt_duration', c_int64),
-        # !
-        ('metadata', POINTER(AVDictionary)),
-        ('decode_error_flags', c_int),
-        ('channels', c_int),
-        ('pkt_size', c_int),
-        ('qscale_table', POINTER(c_int8)),  # Deprecated
-        ('qstride', c_int),  # Deprecated
-        ('qscale_type', c_int),  # Deprecated
-        ('qp_table_buf', POINTER(AVBufferRef)),  # Deprecated
-        ('hw_frames_ctx', POINTER(AVBufferRef)),
-        ('opaque_ref', POINTER(AVBufferRef)),
-        ('crop_top', c_size_t),  # video frames only
-        ('crop_bottom', c_size_t),  # video frames only
-        ('crop_left', c_size_t),  # video frames only
-        ('crop_right', c_size_t),  # video frames only
-        ('private_ref', POINTER(AVBufferRef)),
-    ]
+    pass
 
+AVFrame_Fields = [
+    ('data', POINTER(c_uint8) * AV_NUM_DATA_POINTERS),
+    ('linesize', c_int * AV_NUM_DATA_POINTERS),
+    ('extended_data', POINTER(POINTER(c_uint8))),
+    ('width', c_int),
+    ('height', c_int),
+    ('nb_samples', c_int),
+    ('format', c_int),
+    ('key_frame', c_int),
+    ('pict_type', c_int),
+    ('sample_aspect_ratio', AVRational),
+    ('pts', c_int64),
+    ('pkt_pts', c_int64),  # Deprecated. Removed in 57.
+    ('pkt_dts', c_int64),
+    ('time_base', AVRational),  # (5.x)
+    ('coded_picture_number', c_int),
+    ('display_picture_number', c_int),
+    ('quality', c_int),
+    ('opaque', c_void_p),
+    ('error', c_uint64 * AV_NUM_DATA_POINTERS),  # Deprecated. Removed in 57.
+    ('repeat_pict', c_int),
+    ('interlaced_frame', c_int),
+    ('top_field_first', c_int),
+    ('palette_has_changed', c_int),
+    ('reordered_opaque', c_int64),
+    ('sample_rate', c_int),
+    ('channel_layout', c_uint64),
+    ('buf', POINTER(AVBufferRef) * AV_NUM_DATA_POINTERS),
+    ('extended_buf', POINTER(POINTER(AVBufferRef))),
+    ('nb_extended_buf', c_int),
+    ('side_data', POINTER(POINTER(AVFrameSideData))),
+    ('nb_side_data', c_int),
+    ('flags', c_int),
+    ('color_range', c_int),
+    ('color_primaries', c_int),
+    ('color_trc', c_int),
+    ('colorspace', c_int),
+    ('chroma_location', c_int),
+    ('best_effort_timestamp', c_int64),
+    ('pkt_pos', c_int64),
+    ('pkt_duration', c_int64),
+    # !
+    ('metadata', POINTER(AVDictionary)),
+    ('decode_error_flags', c_int),
+    ('channels', c_int),
+    ('pkt_size', c_int),
+    ('qscale_table', POINTER(c_int8)),  # Deprecated. Removed in 57.
+    ('qstride', c_int),  # Deprecated. Removed in 57.
+    ('qscale_type', c_int),  # Deprecated. Removed in 57.
+    ('qp_table_buf', POINTER(AVBufferRef)),  # Deprecated. Removed in 57.
+    ('hw_frames_ctx', POINTER(AVBufferRef)),
+    ('opaque_ref', POINTER(AVBufferRef)),
+    ('crop_top', c_size_t),  # video frames only
+    ('crop_bottom', c_size_t),  # video frames only
+    ('crop_left', c_size_t),  # video frames only
+    ('crop_right', c_size_t),  # video frames only
+    ('private_ref', POINTER(AVBufferRef)),
+]
+
+compat.add_version_changes('avutil', 56, AVFrame, AVFrame_Fields,
+                           removals=('time_base',))
+
+compat.add_version_changes('avutil', 57, AVFrame, AVFrame_Fields,
+                           removals=('pkt_pts', 'error', 'qscale_table', 'qstride', 'qscale_type', 'qp_table_buf'))
 
 AV_NOPTS_VALUE = -0x8000000000000000
 AV_TIME_BASE = 1000000
@@ -203,8 +225,7 @@ avutil.av_get_bytes_per_sample.restype = c_int
 avutil.av_get_bytes_per_sample.argtypes = [c_int]
 avutil.av_strerror.restype = c_int
 avutil.av_strerror.argtypes = [c_int, c_char_p, c_size_t]
-avutil.av_frame_get_best_effort_timestamp.restype = c_int64
-avutil.av_frame_get_best_effort_timestamp.argtypes = [POINTER(AVFrame)]
+
 avutil.av_image_fill_arrays.restype = c_int
 avutil.av_image_fill_arrays.argtypes = [POINTER(c_uint8) * 4, c_int * 4,
                                         POINTER(c_uint8), c_int, c_int, c_int, c_int]
@@ -214,6 +235,10 @@ avutil.av_dict_set.argtypes = [POINTER(POINTER(AVDictionary)),
 avutil.av_dict_free.argtypes = [POINTER(POINTER(AVDictionary))]
 avutil.av_log_set_level.restype = c_int
 avutil.av_log_set_level.argtypes = [c_uint]
+avutil.av_malloc.restype = c_void_p
+avutil.av_malloc.argtypes = [c_int]
+avutil.av_freep.restype = c_void_p
+avutil.av_freep.argtypes = [c_void_p]
 
 __all__ = [
     'avutil',
diff --git a/pyglet/media/codecs/ffmpeg_lib/libswresample.py b/pyglet/media/codecs/ffmpeg_lib/libswresample.py
index 9c9f3aa..e44200d 100644
--- a/pyglet/media/codecs/ffmpeg_lib/libswresample.py
+++ b/pyglet/media/codecs/ffmpeg_lib/libswresample.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -34,20 +34,27 @@
 # ----------------------------------------------------------------------------
 """Wrapper for include/libswresample/swresample.h
 """
-from ctypes import c_int, c_uint16, c_int32, c_int64, c_uint32, c_uint64
-from ctypes import c_uint8, c_uint, c_double, c_float, c_ubyte, c_size_t, c_char, c_char_p
-from ctypes import c_void_p, addressof, byref, cast, POINTER, CFUNCTYPE, Structure, Union
-from ctypes import create_string_buffer, memmove
+from ctypes import c_int, c_int64
+from ctypes import c_uint8
+from ctypes import c_void_p, POINTER, Structure
 
-import pyglet
 import pyglet.lib
+from pyglet.util import debug_print
+from . import compat
+
+_debug = debug_print('debug_media')
 
 swresample = pyglet.lib.load_library(
     'swresample',
-    win32='swresample-3',
-    darwin='swresample.3'
+    win32=('swresample-4', 'swresample-3'),
+    darwin=('swresample.4', 'swresample.3')
 )
 
+swresample.swresample_version.restype = c_int
+
+compat.set_version('swresample', swresample.swresample_version() >> 16)
+
+
 SWR_CH_MAX = 32
 
 
diff --git a/pyglet/media/codecs/ffmpeg_lib/libswscale.py b/pyglet/media/codecs/ffmpeg_lib/libswscale.py
index ef9e1b2..6ddf884 100644
--- a/pyglet/media/codecs/ffmpeg_lib/libswscale.py
+++ b/pyglet/media/codecs/ffmpeg_lib/libswscale.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -34,20 +34,28 @@
 # ----------------------------------------------------------------------------
 """Wrapper for include/libswscale/swscale.h
 """
-from ctypes import c_int, c_uint16, c_int32, c_int64, c_uint32, c_uint64
-from ctypes import c_uint8, c_uint, c_double, c_float, c_ubyte, c_size_t, c_char, c_char_p
-from ctypes import c_void_p, addressof, byref, cast, POINTER, CFUNCTYPE, Structure, Union
-from ctypes import create_string_buffer, memmove
+from ctypes import POINTER, Structure
+from ctypes import c_int
+from ctypes import c_uint8, c_double
 
-import pyglet
 import pyglet.lib
+from pyglet.util import debug_print
+from . import compat
+
+_debug = debug_print('debug_media')
+
 
 swscale = pyglet.lib.load_library(
     'swscale',
-    win32='swscale-5',
-    darwin='swscale.5'
+    win32=('swscale-6', 'swscale-5'),
+    darwin=('swscale.6', 'swscale.5')
 )
 
+swscale.swscale_version.restype = c_int
+
+compat.set_version('swscale', swscale.swscale_version() >> 16)
+
+
 SWS_FAST_BILINEAR = 1
 
 
diff --git a/pyglet/media/codecs/gstreamer.py b/pyglet/media/codecs/gstreamer.py
index d21a353..1a8410b 100644
--- a/pyglet/media/codecs/gstreamer.py
+++ b/pyglet/media/codecs/gstreamer.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -36,23 +36,29 @@
 """Multi-format decoder using Gstreamer.
 """
 import queue
+import atexit
+import weakref
 import tempfile
+
 from threading import Event, Thread
 
 from ..exceptions import MediaDecodeException
 from .base import StreamingSource, AudioData, AudioFormat, StaticSource
 from . import MediaEncoder, MediaDecoder
 
-import gi
-gi.require_version('Gst', '1.0')
-from gi.repository import Gst, GLib
+try:
+    import gi
+    gi.require_version('Gst', '1.0')
+    from gi.repository import Gst, GLib
+except (ValueError, ImportError) as e:
+    raise ImportError(e)
 
 
 class GStreamerDecodeException(MediaDecodeException):
     pass
 
 
-class GLibMainLoopThread(Thread):
+class _GLibMainLoopThread(Thread):
     """A background Thread for a GLib MainLoop"""
     def __init__(self):
         super().__init__(daemon=True)
@@ -63,13 +69,84 @@ class GLibMainLoopThread(Thread):
         self.mainloop.run()
 
 
+class _MessageHandler:
+    """Message Handler class for GStreamer Sources.
+    
+    This separate class holds a weak reference to the
+    Source, preventing garbage collection issues. 
+    
+    """
+    def __init__(self, source):
+        self.source = weakref.proxy(source)
+
+    def message(self, bus, message):
+        """The main message callback"""
+        if message.type == Gst.MessageType.EOS:
+
+            self.source.queue.put(self.source.sentinal)
+            if not self.source.caps:
+                raise GStreamerDecodeException("Appears to be an unsupported file")
+
+        elif message.type == Gst.MessageType.ERROR:
+            raise GStreamerDecodeException(message.parse_error())
+
+    def notify_caps(self, pad, *args):
+        """notify::caps callback"""
+        self.source.caps = True
+        info = pad.get_current_caps().get_structure(0)
+
+        self.source._duration = pad.get_peer().query_duration(Gst.Format.TIME).duration / Gst.SECOND
+        channels = info.get_int('channels')[1]
+        sample_rate = info.get_int('rate')[1]
+        sample_size = int("".join(filter(str.isdigit, info.get_string('format'))))
+
+        self.source.audio_format = AudioFormat(channels=channels, sample_size=sample_size, sample_rate=sample_rate)
+
+        # Allow GStreamerSource.__init__ to complete:
+        self.source.is_ready.set()
+
+    def pad_added(self, element, pad):
+        """pad-added callback"""
+        name = pad.query_caps(None).to_string()
+        if name.startswith('audio/x-raw'):
+            nextpad = self.source.converter.get_static_pad('sink')
+            if not nextpad.is_linked():
+                self.source.pads = True
+                pad.link(nextpad)
+
+    def no_more_pads(self, element):
+        """Finished Adding pads"""
+        if not self.source.pads:
+            raise GStreamerDecodeException('No Streams Found')
+
+    def new_sample(self, sink):
+        """new-sample callback"""
+        # Pull the sample, and get it's buffer:
+        buffer = sink.emit('pull-sample').get_buffer()
+        # Extract a copy of the memory in the buffer:
+        mem = buffer.extract_dup(0, buffer.get_size())
+        self.source.queue.put(mem)
+        return Gst.FlowReturn.OK
+
+    @staticmethod
+    def unknown_type(uridecodebin, decodebin, caps):
+        """unknown-type callback for unreadable files"""
+        streaminfo = caps.to_string()
+        if not streaminfo.startswith('audio/'):
+            return
+        raise GStreamerDecodeException(streaminfo)
+
+
 class GStreamerSource(StreamingSource):
 
-    _sentinal = object()
+    source_instances = weakref.WeakSet()
+    sentinal = object()
 
     def __init__(self, filename, file=None):
         self._pipeline = Gst.Pipeline()
 
+        msg_handler = _MessageHandler(self)
+
         if file:
             file.seek(0)
             self._file = tempfile.NamedTemporaryFile(buffering=False)
@@ -86,15 +163,15 @@ class GStreamerSource(StreamingSource):
 
         # Set callbacks for EOS and error messages:
         self._pipeline.bus.add_signal_watch()
-        self._pipeline.bus.connect("message", self._message)
+        self._pipeline.bus.connect("message", msg_handler.message)
 
         # Set the file path to load:
         self.filesrc.set_property("location", filename)
 
         # Set decoder callback handlers:
-        self.decoder.connect("pad-added", self._pad_added)
-        self.decoder.connect("no-more-pads", self._no_more_pads)
-        self.decoder.connect("unknown-type", self._unknown_type)
+        self.decoder.connect("pad-added", msg_handler.pad_added)
+        self.decoder.connect("no-more-pads", msg_handler.no_more_pads)
+        self.decoder.connect("unknown-type", msg_handler.unknown_type)
 
         # Set the sink's capabilities and behavior:
         self.appsink.set_property('caps', Gst.Caps.from_string('audio/x-raw,format=S16LE,layout=interleaved'))
@@ -103,7 +180,7 @@ class GStreamerSource(StreamingSource):
         self.appsink.set_property('max-buffers', 0)     # unlimited
         self.appsink.set_property('emit-signals', True)
         # The callback to receive decoded data:
-        self.appsink.connect("new-sample", self._new_sample)
+        self.appsink.connect("new-sample", msg_handler.new_sample)
 
         # Add all components to the pipeline:
         self._pipeline.add(self.filesrc)
@@ -116,98 +193,48 @@ class GStreamerSource(StreamingSource):
         self.converter.link(self.appsink)
 
         # Callback to notify once the sink is ready:
-        self.caps_handler = self.appsink.get_static_pad("sink").connect("notify::caps", self._notify_caps)
+        self.caps_handler = self.appsink.get_static_pad("sink").connect("notify::caps", msg_handler.notify_caps)
 
         # Set by callbacks:
-        self._pads = False
-        self._caps = False
+        self.pads = False
+        self.caps = False
         self._pipeline.set_state(Gst.State.PLAYING)
-        self._queue = queue.Queue(5)
+        self.queue = queue.Queue(5)
         self._finished = Event()
         # Wait until the is_ready event is set by a callback:
-        self._is_ready = Event()
-        if not self._is_ready.wait(timeout=1):
+        self.is_ready = Event()
+        if not self.is_ready.wait(timeout=1):
             raise GStreamerDecodeException('Initialization Error')
 
+        GStreamerSource.source_instances.add(self)
+
     def __del__(self):
+        self.delete()
+
+    def delete(self):
         if hasattr(self, '_file'):
             self._file.close()
 
         try:
+            while not self.queue.empty():
+                self.queue.get_nowait()
+            sink = self.appsink.get_static_pad("sink")
+            if sink.handler_is_connected(self.caps_handler):
+                sink.disconnect(self.caps_handler)
+            self._pipeline.set_state(Gst.State.NULL)
             self._pipeline.bus.remove_signal_watch()
             self.filesrc.set_property("location", None)
-            self.appsink.get_static_pad("sink").disconnect(self.caps_handler)
-            while not self._queue.empty():
-                self._queue.get_nowait()
-            self._pipeline.set_state(Gst.State.NULL)
         except (ImportError, AttributeError):
             pass
 
-    def _notify_caps(self, pad, *args):
-        """notify::caps callback"""
-        self._caps = True
-        info = pad.get_current_caps().get_structure(0)
-
-        self._duration = pad.get_peer().query_duration(Gst.Format.TIME).duration / Gst.SECOND
-        channels = info.get_int('channels')[1]
-        sample_rate = info.get_int('rate')[1]
-        sample_size = int("".join(filter(str.isdigit, info.get_string('format'))))
-
-        self.audio_format = AudioFormat(channels=channels, sample_size=sample_size, sample_rate=sample_rate)
-
-        # Allow __init__ to complete:
-        self._is_ready.set()
-
-    def _pad_added(self, element, pad):
-        """pad-added callback"""
-        name = pad.query_caps(None).to_string()
-        if name.startswith('audio/x-raw'):
-            nextpad = self.converter.get_static_pad('sink')
-            if not nextpad.is_linked():
-                self._pads = True
-                pad.link(nextpad)
-
-    def _no_more_pads(self, element):
-        """Finished Adding pads"""
-        if not self._pads:
-            raise GStreamerDecodeException('No Streams Found')
-
-    def _new_sample(self, sink):
-        """new-sample callback"""
-        # Pull the sample, and get it's buffer:
-        buffer = sink.emit('pull-sample').get_buffer()
-        # Extract a copy of the memory in the buffer:
-        mem = buffer.extract_dup(0, buffer.get_size())
-        self._queue.put(mem)
-        return Gst.FlowReturn.OK
-
-    @staticmethod
-    def _unknown_type(uridecodebin, decodebin, caps):
-        """unknown-type callback for unreadable files"""
-        streaminfo = caps.to_string()
-        if not streaminfo.startswith('audio/'):
-            return
-        raise GStreamerDecodeException(streaminfo)
-
-    def _message(self, bus, message):
-        """The main message callback"""
-        if message.type == Gst.MessageType.EOS:
-
-            self._queue.put(self._sentinal)
-            if not self._caps:
-                raise GStreamerDecodeException("Appears to be an unsupported file")
-
-        elif message.type == Gst.MessageType.ERROR:
-            raise GStreamerDecodeException(message.parse_error())
-
     def get_audio_data(self, num_bytes, compensation_time=0.0):
         if self._finished.is_set():
             return None
 
         data = bytes()
         while len(data) < num_bytes:
-            packet = self._queue.get()
-            if packet == self._sentinal:
+            packet = self.queue.get()
+            if packet == self.sentinal:
                 self._finished.set()
                 break
             data += packet
@@ -222,8 +249,8 @@ class GStreamerSource(StreamingSource):
 
     def seek(self, timestamp):
         # First clear any data in the queue:
-        while not self._queue.empty():
-            self._queue.get_nowait()
+        while not self.queue.empty():
+            self.queue.get_nowait()
 
         self._pipeline.seek_simple(Gst.Format.TIME,
                                    Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
@@ -231,6 +258,16 @@ class GStreamerSource(StreamingSource):
         self._finished.clear()
 
 
+def _cleanup():
+    # At exist, ensure any remaining Source instances are cleaned up.
+    # If this is not done, GStreamer may hang due to dangling callbacks.
+    for src in GStreamerSource.source_instances:
+        src.delete()
+
+
+atexit.register(_cleanup)
+
+
 #########################################
 #   Decoder class:
 #########################################
@@ -239,7 +276,7 @@ class GStreamerDecoder(MediaDecoder):
 
     def __init__(self):
         Gst.init(None)
-        self._glib_loop = GLibMainLoopThread()
+        self._glib_loop = _GLibMainLoopThread()
 
     def get_file_extensions(self):
         return '.mp3', '.flac', '.ogg', '.m4a'
diff --git a/pyglet/media/codecs/pyogg.py b/pyglet/media/codecs/pyogg.py
new file mode 100644
index 0000000..fa02d4b
--- /dev/null
+++ b/pyglet/media/codecs/pyogg.py
@@ -0,0 +1,474 @@
+import pyogg
+
+import os.path
+import warnings
+
+from abc import abstractmethod
+from ctypes import c_void_p, POINTER, c_int, pointer, cast, c_char, c_char_p, CFUNCTYPE, c_ubyte
+from ctypes import memmove, create_string_buffer, byref
+
+from pyglet.media import StreamingSource
+from pyglet.media.codecs import AudioFormat, AudioData, MediaDecoder, StaticSource
+from pyglet.util import debug_print
+
+
+_debug = debug_print('Debug PyOgg codec')
+
+if _debug:
+    if not pyogg.PYOGG_OGG_AVAIL and not pyogg.PYOGG_VORBIS_AVAIL and not pyogg.PYOGG_VORBIS_FILE_AVAIL:
+        warnings.warn("PyOgg determined the ogg/vorbis libraries were not available.")
+
+    if not pyogg.PYOGG_FLAC_AVAIL:
+        warnings.warn("PyOgg determined the flac library was not available.")
+
+    if not pyogg.PYOGG_OPUS_AVAIL and not pyogg.PYOGG_OPUS_FILE_AVAIL:
+        warnings.warn("PyOgg determined the opus libraries were not available.")
+
+if not (
+        pyogg.PYOGG_OGG_AVAIL and not pyogg.PYOGG_VORBIS_AVAIL and not pyogg.PYOGG_VORBIS_FILE_AVAIL) and (
+        not pyogg.PYOGG_OPUS_AVAIL and not pyogg.PYOGG_OPUS_FILE_AVAIL) and not pyogg.PYOGG_FLAC_AVAIL:
+    raise ImportError("PyOgg determined no supported libraries were found")
+
+# Some monkey patching PyOgg for FLAC.
+if pyogg.PYOGG_FLAC_AVAIL:
+    # Original in PyOgg: FLAC__StreamDecoderEofCallback = CFUNCTYPE(FLAC__bool, POINTER(FLAC__StreamDecoder), c_void_p)
+    # FLAC__bool is not valid for this return type (at least for ctypes). Needs to be an int or an error occurs.
+    FLAC__StreamDecoderEofCallback = CFUNCTYPE(c_int, POINTER(pyogg.flac.FLAC__StreamDecoder), c_void_p)
+
+    # Override explicits with c_void_p, so we can support non-seeking FLAC's (CFUNCTYPE does not accept None).
+    pyogg.flac.libflac.FLAC__stream_decoder_init_stream.restype = pyogg.flac.FLAC__StreamDecoderInitStatus
+    pyogg.flac.libflac.FLAC__stream_decoder_init_stream.argtypes = [POINTER(pyogg.flac.FLAC__StreamDecoder),
+                                                                    pyogg.flac.FLAC__StreamDecoderReadCallback,
+                                                                    c_void_p,  # Seek
+                                                                    c_void_p,  # Tell
+                                                                    c_void_p,  # Length
+                                                                    c_void_p,  # EOF
+                                                                    pyogg.flac.FLAC__StreamDecoderWriteCallback,
+                                                                    pyogg.flac.FLAC__StreamDecoderMetadataCallback,
+                                                                    pyogg.flac.FLAC__StreamDecoderErrorCallback,
+                                                                    c_void_p]
+
+
+    def metadata_callback(self, decoder, metadata, client_data):
+        self.bits_per_sample = metadata.contents.data.stream_info.bits_per_sample  # missing from pyogg
+        self.total_samples = metadata.contents.data.stream_info.total_samples
+        self.channels = metadata.contents.data.stream_info.channels
+        self.frequency = metadata.contents.data.stream_info.sample_rate
+
+
+    # Monkey patch metadata callback to include bits per sample as FLAC may rarely deviate from 16 bit.
+    pyogg.FlacFileStream.metadata_callback = metadata_callback
+
+
+class MemoryVorbisObject:
+    def __init__(self, file):
+        self.file = file
+
+        def read_func_cb(ptr, byte_size, size_to_read, datasource):
+            data_size = size_to_read * byte_size
+            data = self.file.read(data_size)
+            read_size = len(data)
+            memmove(ptr, data, read_size)
+            return read_size
+
+        def seek_func_cb(datasource, offset, whence):
+            pos = self.file.seek(offset, whence)
+            return pos
+
+        def close_func_cb(datasource):
+            return 0
+
+        def tell_func_cb(datasource):
+            return self.file.tell()
+
+        self.read_func = pyogg.vorbis.read_func(read_func_cb)
+        self.seek_func = pyogg.vorbis.seek_func(seek_func_cb)
+        self.close_func = pyogg.vorbis.close_func(close_func_cb)
+        self.tell_func = pyogg.vorbis.tell_func(tell_func_cb)
+
+        self.callbacks = pyogg.vorbis.ov_callbacks(self.read_func, self.seek_func, self.close_func, self.tell_func)
+
+
+class UnclosedVorbisFileStream(pyogg.VorbisFileStream):
+    def __del__(self):
+        if self.exists:
+            pyogg.vorbis.ov_clear(byref(self.vf))
+        self.exists = False
+
+    def clean_up(self):
+        """PyOgg calls clean_up on end of data. We may want to loop a sound or replay. Prevent this.
+        Rely on GC (__del__) to clean up objects instead.
+        """
+        return
+
+
+class UnclosedOpusFileStream(pyogg.OpusFileStream):
+    def __del__(self):
+        self.ptr.contents.value = self.ptr_init
+
+        del self.ptr
+
+        if self.of:
+            pyogg.opus.op_free(self.of)
+
+    def clean_up(self):
+        pass
+
+
+class MemoryOpusObject:
+    def __init__(self, filename, file):
+        self.file = file
+        self.filename = filename
+
+        def read_func_cb(stream, buffer, size):
+            data = self.file.read(size)
+            read_size = len(data)
+            memmove(buffer, data, read_size)
+            return read_size
+
+        def seek_func_cb(stream, offset, whence):
+            self.file.seek(offset, whence)
+            return 0
+
+        def tell_func_cb(stream):
+            pos = self.file.tell()
+            return pos
+
+        def close_func_cb(stream):
+            return 0
+
+        self.read_func = pyogg.opus.op_read_func(read_func_cb)
+        self.seek_func = pyogg.opus.op_seek_func(seek_func_cb)
+        self.tell_func = pyogg.opus.op_tell_func(tell_func_cb)
+        self.close_func = pyogg.opus.op_close_func(close_func_cb)
+
+        self.callbacks = pyogg.opus.OpusFileCallbacks(self.read_func, self.seek_func, self.tell_func, self.close_func)
+
+
+class MemoryOpusFileStream(UnclosedOpusFileStream):
+    def __init__(self, filename, file):
+        self.file = file
+
+        self.memory_object = MemoryOpusObject(filename, file)
+
+        self._dummy_fileobj = c_void_p()
+
+        error = c_int()
+
+        self.read_buffer = create_string_buffer(pyogg.PYOGG_STREAM_BUFFER_SIZE)
+
+        self.ptr_buffer = cast(self.read_buffer, POINTER(c_ubyte))
+
+        self.of = pyogg.opus.op_open_callbacks(
+            self._dummy_fileobj,
+            byref(self.memory_object.callbacks),
+            self.ptr_buffer,
+            0,  # Start length
+            byref(error)
+        )
+
+        if error.value != 0:
+            raise pyogg.PyOggError(
+                "file-like object: {} couldn't be processed. Error code : {}".format(filename, error.value))
+
+        self.channels = pyogg.opus.op_channel_count(self.of, -1)
+
+        self.pcm_size = pyogg.opus.op_pcm_total(self.of, -1)
+
+        self.frequency = 48000
+
+        self.bfarr_t = pyogg.opus.opus_int16 * (pyogg.PYOGG_STREAM_BUFFER_SIZE * self.channels * 2)
+
+        self.buffer = cast(pointer(self.bfarr_t()), pyogg.opus.opus_int16_p)
+
+        self.ptr = cast(pointer(self.buffer), POINTER(c_void_p))
+
+        self.ptr_init = self.ptr.contents.value
+
+
+class MemoryVorbisFileStream(UnclosedVorbisFileStream):
+    def __init__(self, path, file):
+        buff = create_string_buffer(pyogg.PYOGG_STREAM_BUFFER_SIZE)
+
+        self.vf = pyogg.vorbis.OggVorbis_File()
+        self.memory_object = MemoryVorbisObject(file)
+
+        error = pyogg.vorbis.libvorbisfile.ov_open_callbacks(buff, self.vf, None, 0, self.memory_object.callbacks)
+        if error != 0:
+            raise pyogg.PyOggError("file couldn't be opened or doesn't exist. Error code : {}".format(error))
+
+        info = pyogg.vorbis.ov_info(byref(self.vf), -1)
+
+        self.channels = info.contents.channels
+
+        self.frequency = info.contents.rate
+
+        array = (c_char * (pyogg.PYOGG_STREAM_BUFFER_SIZE * self.channels))()
+
+        self.buffer_ = cast(pointer(array), c_char_p)
+
+        self.bitstream = c_int()
+        self.bitstream_pointer = pointer(self.bitstream)
+
+        self.exists = True
+
+
+class UnclosedFLACFileStream(pyogg.FlacFileStream):
+    def __init__(self, *args, **kw):
+        super().__init__(*args, **kw)
+        self.seekable = True
+
+    def __del__(self):
+        if self.decoder:
+            pyogg.flac.FLAC__stream_decoder_finish(self.decoder)
+
+
+class MemoryFLACFileStream(UnclosedFLACFileStream):
+    def __init__(self, path, file):
+        self.file = file
+
+        self.file_size = 0
+
+        if getattr(self.file, 'seek', None) and getattr(self.file, 'tell', None):
+            self.seekable = True
+            self.file.seek(0, 2)
+            self.file_size = self.file.tell()
+            self.file.seek(0)
+        else:
+            warnings.warn(f"Warning: {file} file object is not seekable.")
+            self.seekable = False
+
+        self.decoder = pyogg.flac.FLAC__stream_decoder_new()
+
+        self.client_data = c_void_p()
+
+        self.channels = None
+
+        self.frequency = None
+
+        self.total_samples = None
+
+        self.buffer = None
+
+        self.bytes_written = None
+
+        self.write_callback_ = pyogg.flac.FLAC__StreamDecoderWriteCallback(self.write_callback)
+        self.metadata_callback_ = pyogg.flac.FLAC__StreamDecoderMetadataCallback(self.metadata_callback)
+        self.error_callback_ = pyogg.flac.FLAC__StreamDecoderErrorCallback(self.error_callback)
+        self.read_callback_ = pyogg.flac.FLAC__StreamDecoderReadCallback(self.read_callback)
+
+        if self.seekable:
+            self.seek_callback_ = pyogg.flac.FLAC__StreamDecoderSeekCallback(self.seek_callback)
+            self.tell_callback_ = pyogg.flac.FLAC__StreamDecoderTellCallback(self.tell_callback)
+            self.length_callback_ = pyogg.flac.FLAC__StreamDecoderLengthCallback(self.length_callback)
+            self.eof_callback_ = FLAC__StreamDecoderEofCallback(self.eof_callback)
+        else:
+            self.seek_callback_ = None
+            self.tell_callback_ = None
+            self.length_callback_ = None
+            self.eof_callback_ = None
+
+        init_status = pyogg.flac.libflac.FLAC__stream_decoder_init_stream(
+            self.decoder,
+            self.read_callback_,
+            self.seek_callback_,
+            self.tell_callback_,
+            self.length_callback_,
+            self.eof_callback_,
+            self.write_callback_,
+            self.metadata_callback_,
+            self.error_callback_,
+            self.client_data
+        )
+
+        if init_status:  # error
+            raise pyogg.PyOggError("An error occurred when trying to open '{}': {}".format(
+                path, pyogg.flac.FLAC__StreamDecoderInitStatusEnum[init_status]))
+
+        metadata_status = pyogg.flac.FLAC__stream_decoder_process_until_end_of_metadata(self.decoder)
+        if not metadata_status:  # error
+            raise pyogg.PyOggError("An error occured when trying to decode the metadata of {}".format(path))
+
+    def read_callback(self, decoder, buffer, size, data):
+        chunk = size.contents.value
+        data = self.file.read(chunk)
+        read_size = len(data)
+        memmove(buffer, data, read_size)
+
+        size.contents.value = read_size
+
+        if read_size > 0:
+            return 0  # FLAC__STREAM_DECODER_READ_STATUS_CONTINUE
+        elif read_size == 0:
+            return 1  # FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM
+        else:
+            return 2  # FLAC__STREAM_DECODER_READ_STATUS_ABORT
+
+    def seek_callback(self, decoder, offset, data):
+        pos = self.file.seek(offset, 0)
+        if pos < 0:
+            return 1  # FLAC__STREAM_DECODER_SEEK_STATUS_ERROR
+        else:
+            return 0  # FLAC__STREAM_DECODER_SEEK_STATUS_OK
+
+    def tell_callback(self, decoder, offset, data):
+        """Decoder wants to know the current position of the file stream."""
+        pos = self.file.tell()
+        if pos < 0:
+            return 1  # FLAC__STREAM_DECODER_TELL_STATUS_ERROR
+        else:
+            offset.contents.value = pos
+            return 0  # FLAC__STREAM_DECODER_TELL_STATUS_OK
+
+    def length_callback(self, decoder, length, data):
+        """Decoder wants to know the total length of the stream."""
+        if self.file_size == 0:
+            return 1  # FLAC__STREAM_DECODER_LENGTH_STATUS_ERROR
+        else:
+            length.contents.value = self.file_size
+            return 0  # FLAC__STREAM_DECODER_LENGTH_STATUS_OK
+
+    def eof_callback(self, decoder, data):
+        return self.file.tell() >= self.file_size
+
+
+class PyOggSource(StreamingSource):
+    def __init__(self, filename, file):
+        self.filename = filename
+        self.file = file
+        self._stream = None
+        self.sample_size = 16
+
+        self._load_source()
+
+        self.audio_format = AudioFormat(channels=self._stream.channels, sample_size=self.sample_size,
+                                        sample_rate=self._stream.frequency)
+
+    @abstractmethod
+    def _load_source(self):
+        pass
+
+    def get_audio_data(self, num_bytes, compensation_time=0.0):
+        """Data returns as c_short_array instead of LP_c_char or c_ubyte, cast each buffer."""
+        data = self._stream.get_buffer()  # Returns buffer, length or None
+        if data is not None:
+            buff, length = data
+            buff_char_p = cast(buff, POINTER(c_char))
+            return AudioData(buff_char_p[:length], length, 1000, 1000, [])
+
+        return None
+
+    def __del__(self):
+        if self._stream:
+            del self._stream
+
+
+class PyOggFLACSource(PyOggSource):
+
+    def _load_source(self):
+        if self.file:
+            self._stream = MemoryFLACFileStream(self.filename, self.file)
+        else:
+            self._stream = UnclosedFLACFileStream(self.filename)
+
+        self.sample_size = self._stream.bits_per_sample
+        self._duration = self._stream.total_samples / self._stream.frequency
+
+        # Unknown amount of samples. May occur in some sources.
+        if self._stream.total_samples == 0:
+            if _debug:
+                warnings.warn(f"Unknown amount of samples found in {self.filename}. Seeking may be limited.")
+            self._duration_per_frame = 0
+        else:
+            self._duration_per_frame = self._duration / self._stream.total_samples
+
+    def seek(self, timestamp):
+        if self._stream.seekable:
+            # Convert sample to seconds.
+            if self._duration_per_frame:
+                timestamp = max(0.0, min(timestamp, self._duration))
+                position = int(timestamp / self._duration_per_frame)
+            else:  # If we have no duration, we cannot reliably seek. However, 0.0 is still required to play and loop.
+                position = 0
+            seek_succeeded = pyogg.flac.FLAC__stream_decoder_seek_absolute(self._stream.decoder, position)
+            if seek_succeeded is False:
+                warnings.warn(f"Failed to seek FLAC file: {self.filename}")
+        else:
+            warnings.warn(f"Stream is not seekable for FLAC file: {self.filename}.")
+
+
+class PyOggVorbisSource(PyOggSource):
+
+    def _load_source(self):
+        if self.file:
+            self._stream = MemoryVorbisFileStream(self.filename, self.file)
+        else:
+            self._stream = UnclosedVorbisFileStream(self.filename)
+
+        self._duration = pyogg.vorbis.libvorbisfile.ov_time_total(byref(self._stream.vf), -1)
+
+    def get_audio_data(self, num_bytes, compensation_time=0.0):
+        data = self._stream.get_buffer()  # Returns buffer, length or None
+
+        if data is not None:
+            return AudioData(*data, 1000, 1000, [])
+
+        return None
+
+    def seek(self, timestamp):
+        seek_succeeded = pyogg.vorbis.ov_time_seek(self._stream.vf, timestamp)
+        if seek_succeeded != 0:
+            if _debug:
+                warnings.warn(f"Failed to seek file {self.filename} - {seek_succeeded}")
+
+
+class PyOggOpusSource(PyOggSource):
+    def _load_source(self):
+        if self.file:
+            self._stream = MemoryOpusFileStream(self.filename, self.file)
+        else:
+            self._stream = UnclosedOpusFileStream(self.filename)
+
+        self._duration = self._stream.pcm_size / self._stream.frequency
+        self._duration_per_frame = self._duration / self._stream.pcm_size
+
+    def seek(self, timestamp):
+        timestamp = max(0.0, min(timestamp, self._duration))
+        position = int(timestamp / self._duration_per_frame)
+        error = pyogg.opus.op_pcm_seek(self._stream.of, position)
+        if error:
+            warnings.warn(f"Opus stream could not seek properly {error}.")
+
+
+class PyOggDecoder(MediaDecoder):
+    vorbis_exts = ('.ogg',) if pyogg.PYOGG_OGG_AVAIL and pyogg.PYOGG_VORBIS_AVAIL and pyogg.PYOGG_VORBIS_FILE_AVAIL else ()
+    flac_exts = ('.flac',) if pyogg.PYOGG_FLAC_AVAIL else ()
+    opus_exts = ('.opus',) if pyogg.PYOGG_OPUS_AVAIL and pyogg.PYOGG_OPUS_FILE_AVAIL else ()
+    exts = vorbis_exts + flac_exts + opus_exts
+
+    def get_file_extensions(self):
+        return PyOggDecoder.exts
+
+    def decode(self, file, filename, streaming=True):
+        name, ext = os.path.splitext(filename)
+        if ext in PyOggDecoder.vorbis_exts:
+            source = PyOggVorbisSource
+        elif ext in PyOggDecoder.flac_exts:
+            source = PyOggFLACSource
+        elif ext in PyOggDecoder.opus_exts:
+            source = PyOggOpusSource
+        else:
+            raise Exception("Decoder could not find a suitable source to use with this filetype.")
+
+        if streaming:
+            return source(filename, file)
+        else:
+            return StaticSource(source(filename, file))
+
+
+def get_decoders():
+    return [PyOggDecoder()]
+
+
+def get_encoders():
+    return []
diff --git a/pyglet/media/codecs/wave.py b/pyglet/media/codecs/wave.py
index 8296588..172c684 100644
--- a/pyglet/media/codecs/wave.py
+++ b/pyglet/media/codecs/wave.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/codecs/wmf.py b/pyglet/media/codecs/wmf.py
index 9e64f9b..f4022f4 100644
--- a/pyglet/media/codecs/wmf.py
+++ b/pyglet/media/codecs/wmf.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -616,7 +616,7 @@ class WMFSource(Source):
 
         imfmedia.Release()
 
-        uncompressed_mt.SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32)
+        uncompressed_mt.SetGUID(MF_MT_SUBTYPE, MFVideoFormat_ARGB32)
         uncompressed_mt.SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive)
         uncompressed_mt.SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, 1)
 
@@ -868,7 +868,7 @@ class WMFDecoder(MediaDecoder):
                                ])
 
         if WINDOWS_10_ANNIVERSARY_UPDATE_OR_GREATER:
-            extensions.extend(['.mkv', '.flac', '.ogg'])
+            extensions.extend(['.flac'])
 
         return extensions
 
diff --git a/pyglet/media/drivers/__init__.py b/pyglet/media/drivers/__init__.py
index 89565bb..28c6a60 100644
--- a/pyglet/media/drivers/__init__.py
+++ b/pyglet/media/drivers/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -71,11 +71,7 @@ def get_audio_driver():
                 from pyglet.libs.win32.constants import WINDOWS_8_OR_GREATER
                 if WINDOWS_8_OR_GREATER:
                     from . import xaudio2
-                    try:
-                        _audio_driver = xaudio2.create_audio_driver()
-                    except ImportError:
-                        # Occurs when default audio device is not found, and cannot bind.
-                        pass
+                    _audio_driver = xaudio2.create_audio_driver()
                     break
             elif driver_name == 'directsound':
                 from . import directsound
@@ -94,6 +90,9 @@ def get_audio_driver():
                 print('Error importing driver %s:' % driver_name)
                 import traceback
                 traceback.print_exc()
+    else:
+        from . import silent
+        _audio_driver = silent.create_audio_driver()
 
     return _audio_driver
 
diff --git a/pyglet/media/drivers/base.py b/pyglet/media/drivers/base.py
index 6dee7db..9c561e5 100644
--- a/pyglet/media/drivers/base.py
+++ b/pyglet/media/drivers/base.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/drivers/directsound/__init__.py b/pyglet/media/drivers/directsound/__init__.py
index 527d731..4122304 100644
--- a/pyglet/media/drivers/directsound/__init__.py
+++ b/pyglet/media/drivers/directsound/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/drivers/directsound/adaptation.py b/pyglet/media/drivers/directsound/adaptation.py
index e72dad4..40d39d0 100644
--- a/pyglet/media/drivers/directsound/adaptation.py
+++ b/pyglet/media/drivers/directsound/adaptation.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -33,13 +33,13 @@
 # POSSIBILITY OF SUCH DAMAGE.
 # ----------------------------------------------------------------------------
 
-import ctypes
 import math
+import ctypes
 
-import pyglet
 from . import interface
 from pyglet.util import debug_print
 from pyglet.media.events import MediaEvent
+from pyglet.media.mediathreads import PlayerWorkerThread
 from pyglet.media.drivers.base import AbstractAudioDriver, AbstractAudioPlayer
 from pyglet.media.drivers.listener import AbstractListener
 
@@ -56,12 +56,12 @@ def _gain2db(gain):
     Convert linear gain in range [0.0, 1.0] to 100ths of dB.
 
     Power gain = P1/P2
-    dB = 10 log(P1/P2)
+    dB = 2 log(P1/P2)
     dB * 100 = 1000 * log(power gain)
     """
     if gain <= 0:
         return -10000
-    return max(-10000, min(int(1000 * math.log10(min(gain, 1))), 0))
+    return max(-10000, min(int(1000 * math.log2(min(gain, 1))), 0))
 
 
 def _db2gain(db):
@@ -133,11 +133,11 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
         self.driver._ds_driver._native_dsound.Release()
 
     def delete(self):
-        pyglet.clock.unschedule(self._check_refill)
+        self.driver.worker.remove(self)
 
     def play(self):
         assert _debug('DirectSound play')
-        pyglet.clock.schedule_interval(self._check_refill, 0.1)
+        self.driver.worker.add(self)
 
         if not self._playing:
             self._get_audiodata()  # prebuffer if needed
@@ -148,7 +148,7 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
 
     def stop(self):
         assert _debug('DirectSound stop')
-        pyglet.clock.unschedule(self._check_refill)
+        self.driver.worker.remove(self)
 
         if self._playing:
             self._playing = False
@@ -167,11 +167,6 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
         del self._events[:]
         del self._timestamps[:]
 
-    def _check_refill(self, dt):
-        write_size = self.get_write_size()
-        if write_size > self.min_buffer_size:
-            self.refill(write_size)
-
     def refill(self, write_size):
         while write_size > 0:
             assert _debug('refill, write_size =', write_size)
@@ -385,6 +380,9 @@ class DirectSoundDriver(AbstractAudioDriver):
         assert self._ds_driver is not None
         assert self._ds_listener is not None
 
+        self.worker = PlayerWorkerThread()
+        self.worker.start()
+
     def __del__(self):
         self.delete()
 
@@ -403,6 +401,7 @@ class DirectSoundDriver(AbstractAudioDriver):
 
     def delete(self):
         # Make sure the _ds_listener is deleted before the _ds_driver
+        self.worker.stop()
         self._ds_listener = None
 
 
diff --git a/pyglet/media/drivers/directsound/exceptions.py b/pyglet/media/drivers/directsound/exceptions.py
index 6b27563..f22d1f4 100644
--- a/pyglet/media/drivers/directsound/exceptions.py
+++ b/pyglet/media/drivers/directsound/exceptions.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/drivers/directsound/interface.py b/pyglet/media/drivers/directsound/interface.py
index a293e6e..2dff748 100644
--- a/pyglet/media/drivers/directsound/interface.py
+++ b/pyglet/media/drivers/directsound/interface.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/drivers/directsound/lib_dsound.py b/pyglet/media/drivers/directsound/lib_dsound.py
index 49b3737..fe9d618 100644
--- a/pyglet/media/drivers/directsound/lib_dsound.py
+++ b/pyglet/media/drivers/directsound/lib_dsound.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/drivers/listener.py b/pyglet/media/drivers/listener.py
index 78f1dc1..71d43b8 100644
--- a/pyglet/media/drivers/listener.py
+++ b/pyglet/media/drivers/listener.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/drivers/openal/__init__.py b/pyglet/media/drivers/openal/__init__.py
index 561d512..b2734c7 100644
--- a/pyglet/media/drivers/openal/__init__.py
+++ b/pyglet/media/drivers/openal/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/drivers/openal/adaptation.py b/pyglet/media/drivers/openal/adaptation.py
index 2d6f059..d73120b 100644
--- a/pyglet/media/drivers/openal/adaptation.py
+++ b/pyglet/media/drivers/openal/adaptation.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -35,11 +35,11 @@
 
 import weakref
 
-import pyglet
 from . import interface
 from pyglet.util import debug_print
-from pyglet.media.drivers.base import AbstractAudioDriver, AbstractAudioPlayer
 from pyglet.media.events import MediaEvent
+from pyglet.media.drivers.base import AbstractAudioDriver, AbstractAudioPlayer
+from pyglet.media.mediathreads import PlayerWorkerThread
 from pyglet.media.drivers.listener import AbstractListener
 
 _debug = debug_print('debug_media')
@@ -47,9 +47,7 @@ _debug = debug_print('debug_media')
 
 class OpenALDriver(AbstractAudioDriver):
     def __init__(self, device_name=None):
-        super(OpenALDriver, self).__init__()
-
-        # TODO devices must be enumerated on Windows, otherwise 1.0 context is returned.
+        super().__init__()
 
         self.device = interface.OpenALDevice(device_name)
         self.context = self.device.create_context()
@@ -57,6 +55,9 @@ class OpenALDriver(AbstractAudioDriver):
 
         self._listener = OpenALListener(self)
 
+        self.worker = PlayerWorkerThread()
+        self.worker.start()
+
     def __del__(self):
         assert _debug("Delete OpenALDriver")
         self.delete()
@@ -66,7 +67,7 @@ class OpenALDriver(AbstractAudioDriver):
         return OpenALAudioPlayer(self, source, player)
 
     def delete(self):
-        # Delete the context first
+        self.worker.stop()
         self.context = None
 
     def have_version(self, major, minor):
@@ -166,7 +167,7 @@ class OpenALAudioPlayer(AbstractAudioPlayer):
         self.delete()
 
     def delete(self):
-        pyglet.clock.unschedule(self._check_refill)
+        self.driver.worker.remove(self)
         self.alsource = None
 
     @property
@@ -184,11 +185,11 @@ class OpenALAudioPlayer(AbstractAudioPlayer):
         self._playing = True
         self._clearing = False
 
-        pyglet.clock.schedule_interval_soft(self._check_refill, 0.1)
+        self.driver.worker.add(self)
 
     def stop(self):
+        self.driver.worker.remove(self)
         assert _debug('OpenALAudioPlayer.stop()')
-        pyglet.clock.unschedule(self._check_refill)
         assert self.driver is not None
         assert self.alsource is not None
         self.alsource.pause()
@@ -200,7 +201,7 @@ class OpenALAudioPlayer(AbstractAudioPlayer):
         assert self.driver is not None
         assert self.alsource is not None
 
-        super(OpenALAudioPlayer, self).clear()
+        super().clear()
         self.alsource.stop()
         self._handle_processed_buffers()
         self.alsource.clear()
@@ -216,19 +217,13 @@ class OpenALAudioPlayer(AbstractAudioPlayer):
         del self._buffer_sizes[:]
         del self._buffer_timestamps[:]
 
-    def _check_refill(self, dt=0):
-        write_size = self.get_write_size()
-        if write_size > self.min_buffer_size:
-            self.refill(write_size)
-
     def _update_play_cursor(self):
         assert self.driver is not None
         assert self.alsource is not None
 
         self._handle_processed_buffers()
 
-        # Update play cursor using buffer cursor + estimate into current
-        # buffer
+        # Update play cursor using buffer cursor + estimate into current buffer
         if self._clearing:
             self._play_cursor = self._buffer_cursor
         else:
diff --git a/pyglet/media/drivers/openal/interface.py b/pyglet/media/drivers/openal/interface.py
index 6da47eb..0ee1af0 100644
--- a/pyglet/media/drivers/openal/interface.py
+++ b/pyglet/media/drivers/openal/interface.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -39,7 +39,7 @@ from collections import namedtuple
 
 from . import lib_openal as al
 from . import lib_alc as alc
-import pyglet
+
 from pyglet.util import debug_print
 from pyglet.media.exceptions import MediaException
 
@@ -54,11 +54,9 @@ class OpenALException(MediaException):
 
     def __str__(self):
         if self.error_code is None:
-            return 'OpenAL Exception: {}'.format(self.message)
+            return f'OpenAL Exception: {self.message}'
         else:
-            return 'OpenAL Exception [{}: {}]: {}'.format(self.error_code,
-                                                          self.error_string,
-                                                          self.message)
+            return f'OpenAL Exception [{self.error_code}: {self.error_string}]: {self.message}'
 
 
 class OpenALObject:
@@ -130,7 +128,7 @@ class OpenALDevice(OpenALObject):
         error_code = alc.alcGetError(self._al_device)
         if error_code != 0:
             error_string = alc.alcGetString(self._al_device, error_code)
-            #TODO: Fix return type in generated code?
+            # TODO: Fix return type in generated code?
             error_string = ctypes.cast(error_string, ctypes.c_char_p)
             raise OpenALException(message=message,
                                   error_code=error_code,
@@ -193,7 +191,6 @@ class OpenALSource(OpenALObject):
             # Only delete source if the context still exists
             al.alDeleteSources(1, self._al_source)
             self._check_error('Failed to delete source.')
-            # TODO: delete buffers in use
             self.buffer_pool.clear()
             self._al_source = None
 
@@ -473,7 +470,7 @@ class OpenALBuffer(OpenALObject):
         try:
             al_format = self._format_map[(audio_format.channels, audio_format.sample_size)]
         except KeyError:
-            raise MediaException('Unsupported sample size')
+            raise MediaException(f"OpenAL does not support '{audio_format.sample_size}bit' audio.")
 
         al.alBufferData(self._al_buffer,
                         al_format,
@@ -516,7 +513,7 @@ class OpenALBufferPool(OpenALObject):
             if self._buffers:
                 b = self._buffers.pop()
             else:
-                b = self.create_buffer()
+                b = self._create_buffer()
             if b.is_valid:
                 # Protect against implementations that DO free buffers
                 # when they delete a source - carry on.
@@ -530,10 +527,9 @@ class OpenALBufferPool(OpenALObject):
         if buf.is_valid:
             self._buffers.append(buf)
 
-    def create_buffer(self):
+    def _create_buffer(self):
         """Create a new buffer."""
         al_buffer = al.ALuint()
         al.alGenBuffers(1, al_buffer)
         self._check_error('Error allocating buffer.')
         return OpenALBuffer(al_buffer, self.context)
-
diff --git a/pyglet/media/drivers/openal/lib_alc.py b/pyglet/media/drivers/openal/lib_alc.py
index 1952452..b1590c4 100644
--- a/pyglet/media/drivers/openal/lib_alc.py
+++ b/pyglet/media/drivers/openal/lib_alc.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/drivers/openal/lib_openal.py b/pyglet/media/drivers/openal/lib_openal.py
index 1744cd9..850e55a 100644
--- a/pyglet/media/drivers/openal/lib_openal.py
+++ b/pyglet/media/drivers/openal/lib_openal.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/drivers/pulse/__init__.py b/pyglet/media/drivers/pulse/__init__.py
index 3222357..3d5049c 100644
--- a/pyglet/media/drivers/pulse/__init__.py
+++ b/pyglet/media/drivers/pulse/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/drivers/pulse/adaptation.py b/pyglet/media/drivers/pulse/adaptation.py
index 317a588..4856cdb 100644
--- a/pyglet/media/drivers/pulse/adaptation.py
+++ b/pyglet/media/drivers/pulse/adaptation.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -84,7 +84,6 @@ class PulseAudioDriver(AbstractAudioDriver):
 
     def dump_debug_info(self):
         print('Client version: ', pa.pa_get_library_version())
-
         print('Server:         ', self.context.server)
         print('Protocol:       ', self.context.protocol_version)
         print('Server protocol:', self.context.server_protocol_version)
diff --git a/pyglet/media/drivers/pulse/interface.py b/pyglet/media/drivers/pulse/interface.py
index 8f57abb..7e8e62e 100644
--- a/pyglet/media/drivers/pulse/interface.py
+++ b/pyglet/media/drivers/pulse/interface.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/drivers/pulse/lib_pulseaudio.py b/pyglet/media/drivers/pulse/lib_pulseaudio.py
index a6b3dd7..9c050fc 100644
--- a/pyglet/media/drivers/pulse/lib_pulseaudio.py
+++ b/pyglet/media/drivers/pulse/lib_pulseaudio.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/drivers/silent/__init__.py b/pyglet/media/drivers/silent/__init__.py
index fc11b39..f487cfe 100644
--- a/pyglet/media/drivers/silent/__init__.py
+++ b/pyglet/media/drivers/silent/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/drivers/silent/adaptation.py b/pyglet/media/drivers/silent/adaptation.py
index fc66bf9..e726bd7 100644
--- a/pyglet/media/drivers/silent/adaptation.py
+++ b/pyglet/media/drivers/silent/adaptation.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/drivers/xaudio2/__init__.py b/pyglet/media/drivers/xaudio2/__init__.py
index 642c21e..6021c17 100644
--- a/pyglet/media/drivers/xaudio2/__init__.py
+++ b/pyglet/media/drivers/xaudio2/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/drivers/xaudio2/adaptation.py b/pyglet/media/drivers/xaudio2/adaptation.py
index 2e434da..cbe8eba 100644
--- a/pyglet/media/drivers/xaudio2/adaptation.py
+++ b/pyglet/media/drivers/xaudio2/adaptation.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -69,7 +69,7 @@ class XAudio2AudioPlayer(AbstractAudioPlayer):
         self._xa2_driver = xa2_driver
 
         # If cleared, we need to check when it's done clearing.
-        self._clearing = False
+        self._flushing = False
 
         # If deleted, we need to make sure it's done deleting.
         self._deleted = False
@@ -94,7 +94,7 @@ class XAudio2AudioPlayer(AbstractAudioPlayer):
         # This will be True if the last buffer has already been submitted.
         self.buffer_end_submitted = False
 
-        self._buffers = []
+        self._buffers = []  # Current buffers in queue waiting to be played.
 
         self._xa2_source_voice = self._xa2_driver.get_source_voice(source, self)
 
@@ -122,12 +122,16 @@ class XAudio2AudioPlayer(AbstractAudioPlayer):
         if self._xa2_source_voice:
             self._deleted = True
 
+            if not self._buffers:
+                self._xa2_driver.return_voice(self._xa2_source_voice)
+
+
     def play(self):
         assert _debug('XAudio2 play')
 
         if not self._playing:
-            if not self._clearing:
-                self._playing = True
+            self._playing = True
+            if not self._flushing:
                 self._xa2_source_voice.play()
 
         assert _debug('return XAudio2 play')
@@ -148,8 +152,11 @@ class XAudio2AudioPlayer(AbstractAudioPlayer):
         self._play_cursor = 0
         self._write_cursor = 0
         self.buffer_end_submitted = False
-        self._clearing = True
         self._deleted = False
+
+        if self._buffers:
+            self._flushing = True
+
         self._xa2_source_voice.flush()
         self._buffers.clear()
         del self._events[:]
@@ -157,15 +164,16 @@ class XAudio2AudioPlayer(AbstractAudioPlayer):
 
     def _restart(self, dt):
         """Prefill audio and attempt to replay audio."""
-        if self._xa2_source_voice:
-            self.prefill_audio()
-            self.play()
+        if self._playing and self._xa2_source_voice:
+            self.refill_source_player()
+            self._xa2_source_voice.play()
 
     def refill_source_player(self):
         """Obtains audio data from the source, puts it into a buffer to submit to the voice.
         Unlike the other drivers this does not carve pieces of audio from the buffer and slowly
         consume it. This submits the buffer retrieved from the decoder in it's entirety.
         """
+
         buffers_queued = self._xa2_source_voice.buffers_queued
 
         # Free any buffers that have ended.
@@ -175,11 +183,14 @@ class XAudio2AudioPlayer(AbstractAudioPlayer):
             self._play_cursor += buffer.AudioBytes
             del buffer  # Does this remove AudioData within the buffer? Let GC remove or explicit remove?
 
-        # We have to wait for all of the buffers we are clearing to end before we restart next source.
-        # When we reach 0, schedule restart.
-        if self._clearing:
+        # We have to wait for all of the buffers we are flushing to end before we restart next buffer.
+        # When voice reaches 0 buffers, it is available for re-use.
+        if self._flushing:
             if buffers_queued == 0:
-                self._clearing = False
+                self._flushing = False
+
+                # This is required because the next call to play will come before all flushes are done.
+                # Restart at next available opportunity.
                 pyglet.clock.schedule_once(self._restart, 0)
             return
 
@@ -315,7 +326,9 @@ class XAudio2AudioPlayer(AbstractAudioPlayer):
             self._xa2_source_voice.cone_outside_volume = cone_outer_gain
 
     def prefill_audio(self):
-        self.refill_source_player()
+        # Cannot refill during a flush. Schedule will handle it.
+        if not self._flushing:
+            self.refill_source_player()
 
 
 class XAudio2Driver(AbstractAudioDriver):
diff --git a/pyglet/media/drivers/xaudio2/interface.py b/pyglet/media/drivers/xaudio2/interface.py
index 3a54aa1..147676e 100644
--- a/pyglet/media/drivers/xaudio2/interface.py
+++ b/pyglet/media/drivers/xaudio2/interface.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -76,6 +76,8 @@ class XAudio2Driver:
 
         self._players = []  # Only used for resetting/restoring xaudio2. Store players to callback.
 
+        self._create_xa2()
+
         if self.restart_on_error:
             audio_devices = get_audio_device_manager()
             if audio_devices:
@@ -87,8 +89,6 @@ class XAudio2Driver:
 
                 pyglet.clock.schedule_interval_soft(self._check_state, 0.5)
 
-        self._create_xa2()
-
     def _check_state(self, dt):
         """Hack/workaround, you cannot shutdown/create XA2 within a COM callback, set a schedule to check state."""
         if self._dead is True:
@@ -115,7 +115,11 @@ class XAudio2Driver:
 
     def _create_xa2(self, device_id=None):
         self._xaudio2 = lib.IXAudio2()
-        lib.XAudio2Create(ctypes.byref(self._xaudio2), 0, self.processor)
+
+        try:
+            lib.XAudio2Create(ctypes.byref(self._xaudio2), 0, self.processor)
+        except OSError:
+            raise ImportError("XAudio2 driver could not be initialized.")
 
         if _debug:
             # Debug messages are found in Windows Event Viewer, you must enable event logging:
@@ -283,7 +287,7 @@ class XAudio2Driver:
         """ Get a source voice from the pool. Source voice creation can be slow to create/destroy. So pooling is
             recommended. We pool based on audio channels as channels must be the same as well as frequency.
             Source voice handles all of the audio playing and state for a single source."""
-        voice_key = (source.audio_format.channels, source.audio_format.sample_size)
+        voice_key = (source.audio_format.channels, source.audio_format.sample_size, source.audio_format.sample_rate)
         if len(self._voice_pool[voice_key]) > 0:
             source_voice = self._voice_pool[voice_key].pop(0)
             source_voice.acquired(player)
@@ -319,7 +323,7 @@ class XAudio2Driver:
     def return_voice(self, voice):
         """Reset a voice and return it to the pool."""
         voice.reset()
-        voice_key = (voice.audio_format.channels, voice.audio_format.sample_size)
+        voice_key = (voice.audio_format.channels, voice.audio_format.sample_size, voice.audio_format.sample_rate)
         self._voice_pool[voice_key].append(voice)
 
         if voice.is_emitter:
diff --git a/pyglet/media/events.py b/pyglet/media/events.py
index ff3a63d..1ddbc93 100644
--- a/pyglet/media/events.py
+++ b/pyglet/media/events.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/exceptions.py b/pyglet/media/exceptions.py
index 94312e5..64673d6 100644
--- a/pyglet/media/exceptions.py
+++ b/pyglet/media/exceptions.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/instrumentation.py b/pyglet/media/instrumentation.py
index 324dca2..81e4115 100644
--- a/pyglet/media/instrumentation.py
+++ b/pyglet/media/instrumentation.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/media/mediathreads.py b/pyglet/media/mediathreads.py
new file mode 100644
index 0000000..42667b5
--- /dev/null
+++ b/pyglet/media/mediathreads.py
@@ -0,0 +1,186 @@
+# ----------------------------------------------------------------------------
+# pyglet
+# Copyright (c) 2006-2008 Alex Holkner
+# Copyright (c) 2008-2022 pyglet contributors
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+#  * Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+#  * Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in
+#    the documentation and/or other materials provided with the
+#    distribution.
+#  * Neither the name of pyglet nor the names of its
+#    contributors may be used to endorse or promote products
+#    derived from this software without specific prior written
+#    permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# ----------------------------------------------------------------------------
+
+import time
+import atexit
+import threading
+
+import pyglet
+
+from pyglet.util import debug_print
+
+
+_debug = debug_print('debug_media')
+
+
+class MediaThread:
+    """A thread that cleanly exits on interpreter shutdown, and provides
+    a sleep method that can be interrupted and a termination method.
+
+    :Ivariables:
+        `_condition` : threading.Condition
+            Lock _condition on all instance variables.
+        `_stopped` : bool
+            True if `stop` has been called.
+
+    """
+    _threads = set()
+    _threads_lock = threading.Lock()
+
+    def __init__(self):
+        self._thread = threading.Thread(target=self._thread_run, daemon=True)
+        self._condition = threading.Condition()
+        self._stopped = False
+
+    def run(self):
+        raise NotImplementedError
+
+    def _thread_run(self):
+        if pyglet.options['debug_trace']:
+            pyglet._install_trace()
+
+        with self._threads_lock:
+            self._threads.add(self)
+        self.run()
+        with self._threads_lock:
+            self._threads.remove(self)
+
+    def start(self):
+        self._thread.start()
+
+    def stop(self):
+        """Stop the thread and wait for it to terminate.
+
+        The `stop` instance variable is set to ``True`` and the condition is
+        notified.  It is the responsibility of the `run` method to check
+        the value of `stop` after each sleep or wait and to return if set.
+        """
+        assert _debug('MediaThread.stop()')
+        with self._condition:
+            self._stopped = True
+            self._condition.notify()
+        self._thread.join()
+
+    def sleep(self, timeout):
+        """Wait for some amount of time, or until notified.
+
+        :Parameters:
+            `timeout` : float
+                Time to wait, in seconds.
+
+        """
+        assert _debug('MediaThread.sleep(%r)' % timeout)
+        with self._condition:
+            if not self._stopped:
+                self._condition.wait(timeout)
+
+    def notify(self):
+        """Interrupt the current sleep operation.
+
+        If the thread is currently sleeping, it will be woken immediately,
+        instead of waiting the full duration of the timeout.
+        """
+        assert _debug('MediaThread.notify()')
+        with self._condition:
+            self._condition.notify()
+
+    @classmethod
+    def atexit(cls):
+        with cls._threads_lock:
+            threads = list(cls._threads)
+        for thread in threads:
+            thread.stop()
+
+
+atexit.register(MediaThread.atexit)
+
+
+class PlayerWorkerThread(MediaThread):
+    """Worker thread for refilling players."""
+
+    # Time to wait if there are players, but they're all full:
+    _nap_time = 0.05
+
+    def __init__(self):
+        super().__init__()
+        self.players = set()
+
+    def run(self):
+        while True:
+            # This is a big lock, but ensures a player is not deleted while
+            # we're processing it -- this saves on extra checks in the
+            # player's methods that would otherwise have to check that it's
+            # still alive.
+            with self._condition:
+                assert _debug('PlayerWorkerThread: woke up @{}'.format(time.time()))
+                if self._stopped:
+                    break
+                sleep_time = -1
+
+                if self.players:
+                    filled = False
+                    for player in list(self.players):
+                        write_size = player.get_write_size()
+                        if write_size > player.min_buffer_size:
+                            player.refill(write_size)
+                            filled = True
+                    if not filled:
+                        sleep_time = self._nap_time
+                else:
+                    assert _debug('PlayerWorkerThread: No active players')
+                    sleep_time = None   # sleep until a player is added
+
+                if sleep_time != -1:
+                    self.sleep(sleep_time)
+                else:
+                    # We MUST sleep, or we will starve pyglet's main loop.  It
+                    # also looks like if we don't sleep enough, we'll starve out
+                    # various updates that stop us from properly removing players
+                    # that should be removed.
+                    self.sleep(self._nap_time)
+
+    def add(self, player):
+        assert player is not None
+        assert _debug('PlayerWorkerThread: player added')
+        with self._condition:
+            self.players.add(player)
+            self._condition.notify()
+
+    def remove(self, player):
+        assert _debug('PlayerWorkerThread: player removed')
+        with self._condition:
+            if player in self.players:
+                self.players.remove(player)
+            self._condition.notify()
diff --git a/pyglet/media/player.py b/pyglet/media/player.py
index 668d223..333fcff 100644
--- a/pyglet/media/player.py
+++ b/pyglet/media/player.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -260,6 +260,8 @@ class Player(pyglet.event.EventDispatcher):
 
         The internal audio player and the texture will be deleted.
         """
+        if self._source:
+            self.source.is_player_source = False
         if self._audio_player:
             self._audio_player.delete()
             self._audio_player = None
@@ -476,6 +478,11 @@ class Player(pyglet.event.EventDispatcher):
 
             pyglet.clock.schedule_once(self._video_finished, 0)
             return
+        elif ts > time:
+            # update_texture called too early (probably manually!)
+            pyglet.clock.schedule_once(self.update_texture, ts - time)
+            return
+
 
         image = source.get_next_video_frame()
         if image is not None:
diff --git a/pyglet/media/synthesis.py b/pyglet/media/synthesis.py
index 6a0fec8..7fc4744 100644
--- a/pyglet/media/synthesis.py
+++ b/pyglet/media/synthesis.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/model/__init__.py b/pyglet/model/__init__.py
index 061d61a..948cdb9 100644
--- a/pyglet/model/__init__.py
+++ b/pyglet/model/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/model/codecs/__init__.py b/pyglet/model/codecs/__init__.py
index 635d29f..0d8f0d4 100644
--- a/pyglet/model/codecs/__init__.py
+++ b/pyglet/model/codecs/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/model/codecs/gltf.py b/pyglet/model/codecs/gltf.py
index bd23f59..bbfa40e 100644
--- a/pyglet/model/codecs/gltf.py
+++ b/pyglet/model/codecs/gltf.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/model/codecs/obj.py b/pyglet/model/codecs/obj.py
index b5d8064..665dea0 100644
--- a/pyglet/model/codecs/obj.py
+++ b/pyglet/model/codecs/obj.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -67,6 +67,8 @@ def load_material_library(filename):
     opacity = 1.0
     texture_name = None
 
+    matlib = {}
+
     for line in file:
         if line.startswith('#'):
             continue
@@ -75,7 +77,13 @@ def load_material_library(filename):
             continue
 
         if values[0] == 'newmtl':
+            if name is not None:
+                # save previous material
+                for item in (diffuse, ambient, specular, emission):
+                    item.append(opacity)
+                matlib[name] = Material(name, diffuse, ambient, specular, emission, shininess, texture_name)
             name = values[1]
+
         elif name is None:
             raise ModelDecodeException('Expected "newmtl" in '.format(filename))
 
@@ -103,7 +111,9 @@ def load_material_library(filename):
     for item in (diffuse, ambient, specular, emission):
         item.append(opacity)
 
-    return Material(name, diffuse, ambient, specular, emission, shininess, texture_name)
+    matlib[name] = Material(name, diffuse, ambient, specular, emission, shininess, texture_name)
+
+    return matlib
 
 
 def parse_obj_file(filename, file=None):
@@ -156,8 +166,7 @@ def parse_obj_file(filename, file=None):
 
         elif values[0] == 'mtllib':
             material_abspath = os.path.join(location, values[1])
-            material = load_material_library(filename=material_abspath)
-            materials[material.name] = material
+            materials = load_material_library(filename=material_abspath)            
 
         elif values[0] in ('usemtl', 'usemat'):
             material = materials.get(values[1])
diff --git a/pyglet/resource.py b/pyglet/resource.py
index 47c86a9..e74a6aa 100644
--- a/pyglet/resource.py
+++ b/pyglet/resource.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/shapes.py b/pyglet/shapes.py
index 90dd69e..7ffce2c 100644
--- a/pyglet/shapes.py
+++ b/pyglet/shapes.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -36,12 +36,12 @@
 """2D shapes.
 
 This module provides classes for a variety of simplistic 2D shapes,
-such as Rectangles, Circles, and Lines. These shapes are are made
+such as Rectangles, Circles, and Lines. These shapes are made
 internally from OpenGL primitives, and provide excellent performance
 when drawn as part of a :py:class:`~pyglet.graphics.Batch`.
 Convenience methods are provided for positioning, changing color
-and opacity, and rotation (where applicible). To create more
-complex shapes than what is provided here, the lower evel
+and opacity, and rotation (where applicable). To create more
+complex shapes than what is provided here, the lower level
 graphics API is more appropriate.
 See the :ref:`guide_graphics` for more details.
 
@@ -60,6 +60,7 @@ A simple example of drawing shapes::
     rectangle.rotation = 33
     line = shapes.Line(100, 100, 100, 200, width=19, batch=batch)
     line2 = shapes.Line(150, 150, 444, 111, width=4, color=(200, 20, 20), batch=batch)
+    star = shapes.Star(800, 400, 60, 40, num_spikes=20, color=(255, 255, 0), batch=batch)
 
     @window.event
     def on_draw():
@@ -76,11 +77,37 @@ A simple example of drawing shapes::
 import math
 
 from pyglet.gl import GL_COLOR_BUFFER_BIT, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA
-from pyglet.gl import GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_LINES, GL_BLEND
+from pyglet.gl import GL_TRIANGLES, GL_LINES, GL_BLEND
 from pyglet.gl import glPushAttrib, glPopAttrib, glBlendFunc, glEnable, glDisable
 from pyglet.graphics import Group, Batch
 
 
+def _rotate(vertices, angle, x, y):
+    """Rotate the vertices by the angle around x, y.
+
+    :Parameters:
+        `vertices` : list
+            A list of (x, y) tuples, representing each vertex to rotate.
+        `angle` : float
+            The angle of the rotation in degrees.
+        `x` : int or float
+            X coordinate of the center of rotation.
+        `y` : int or float
+            Y coordinate of the center of rotation.
+    """
+    r = -math.radians(angle)
+    cr = math.cos(r)
+    sr = math.sin(r)
+
+    rotated_vertices = []
+    for vertex in vertices:
+        rotated_x = (vertex[0] - x) * cr - (vertex[1] - y) * sr + x
+        rotated_y = (vertex[1] - y) * cr + (vertex[0] - x) * sr + y
+        rotated_vertices.append((rotated_x, rotated_y))
+
+    return rotated_vertices
+
+
 class _ShapeGroup(Group):
     """Shared Shape rendering Group.
 
@@ -142,11 +169,8 @@ class _ShapeBase:
     _vertex_list = None
 
     def __del__(self):
-        try:
-            if self._vertex_list is not None:
-                self._vertex_list.delete()
-        except:
-            pass
+        if self._vertex_list is not None:
+            self._vertex_list.delete()
 
     def _update_position(self):
         raise NotImplementedError
@@ -269,7 +293,7 @@ class _ShapeBase:
 
     @color.setter
     def color(self, values):
-        self._rgb = list(map(int, values))
+        self._rgb = tuple(map(int, values))
         self._update_color()
 
     @property
@@ -308,45 +332,103 @@ class _ShapeBase:
 
 
 class Arc(_ShapeBase):
-    def __init__(self, x, y, radius, segments=25, angle=math.pi * 2, color=(255, 255, 255), batch=None, group=None):
-        # TODO: Finish this shape and add docstring.
+    def __init__(self, x, y, radius, segments=None, angle=math.tau, start_angle=0,
+                 closed=False, color=(255, 255, 255), batch=None, group=None):
+        """Create an Arc.
+
+        The Arc's anchor point (x, y) defaults to it's center.
+
+        :Parameters:
+            `x` : float
+                X coordinate of the circle.
+            `y` : float
+                Y coordinate of the circle.
+            `radius` : float
+                The desired radius.
+            `segments` : int
+                You can optionally specify how many distinct line segments
+                the arc should be made from. If not specified it will be
+                automatically calculated using the formula:
+                `max(14, int(radius / 1.25))`.
+            `angle` : float
+                The angle of the arc, in radians. Defaults to tau (pi * 2),
+                which is a full circle.
+            `start_angle` : float
+                The start angle of the arc, in radians. Defaults to 0.
+            `closed` : bool
+                If True, the ends of the arc will be connected with a line.
+                defaults to False.
+            `color` : (int, int, int)
+                The RGB color of the circle, specified as a tuple of
+                three ints in the range of 0-255.
+            `batch` : `~pyglet.graphics.Batch`
+                Optional batch to add the circle to.
+            `group` : `~pyglet.graphics.Group`
+                Optional parent group of the circle.
+        """
         self._x = x
         self._y = y
         self._radius = radius
-        self._segments = segments
+        self._segments = segments or max(14, int(radius / 1.25))
+        self._num_verts = self._segments * 2 + (2 if closed else 0)
+
         self._rgb = color
         self._angle = angle
+        self._start_angle = start_angle
+        self._closed = closed
+        self._rotation = 0
 
         self._batch = batch or Batch()
         self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group)
 
-        self._vertex_list = self._batch.add(self._segments * 2, GL_LINES, self._group, 'v2f', 'c4B')
+        self._vertex_list = self._batch.add(self._num_verts, GL_LINES, self._group, 'v2f', 'c4B')
         self._update_position()
         self._update_color()
 
     def _update_position(self):
         if not self._visible:
-            vertices = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
+            vertices = (0,) * self._segments * 4
         else:
             x = self._x + self._anchor_x
             y = self._y + self._anchor_y
             r = self._radius
-            tau_segs = self._angle / (self._segments - 1)
+            tau_segs = self._angle / self._segments
+            start_angle = self._start_angle - math.radians(self._rotation)
 
-            # Calcuate the outer points of the arc:
-            points = [(x + (r * math.cos(i * tau_segs)),
-                       y + (r * math.sin(i * tau_segs))) for i in range(self._segments)]
+            # Calculate the outer points of the arc:
+            points = [(x + (r * math.cos((i * tau_segs) + start_angle)),
+                       y + (r * math.sin((i * tau_segs) + start_angle))) for i in range(self._segments + 1)]
 
             # Create a list of doubled-up points from the points:
             vertices = []
-            for i, point in enumerate(points):
-                line_points = *points[i - 1], *point
+            for i in range(len(points) - 1):
+                line_points = *points[i], *points[i + 1]
                 vertices.extend(line_points)
 
+            if self._closed:
+                chord_points = *points[-1], *points[0]
+                vertices.extend(chord_points)
+
         self._vertex_list.vertices[:] = vertices
 
     def _update_color(self):
-        self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * self._segments * 2
+        self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * self._num_verts
+
+    @property
+    def rotation(self):
+        """Clockwise rotation of the arc, in degrees.
+
+        The arc will be rotated about its (anchor_x, anchor_y)
+        position.
+
+        :type: float
+        """
+        return self._rotation
+
+    @rotation.setter
+    def rotation(self, rotation):
+        self._rotation = rotation
+        self._update_position()
 
     def draw(self):
         """Draw the shape at its current position.
@@ -371,9 +453,10 @@ class Circle(_ShapeBase):
             `radius` : float
                 The desired radius.
             `segments` : int
-                You can optionally specifify how many distict triangles
-                the circle should be made from. If not specified, it will
-                be automatically calculated based on the radius.
+                You can optionally specify how many distinct triangles
+                the circle should be made from. If not specified it will
+                be automatically calculated using the formula:
+                `max(14, int(radius / 1.25))`.
             `color` : (int, int, int)
                 The RGB color of the circle, specified as a tuple of
                 three ints in the range of 0-255.
@@ -385,7 +468,7 @@ class Circle(_ShapeBase):
         self._x = x
         self._y = y
         self._radius = radius
-        self._segments = segments or int(radius / 1.25)
+        self._segments = segments or max(14, int(radius / 1.25))
         self._rgb = color
 
         self._batch = batch or Batch()
@@ -404,11 +487,11 @@ class Circle(_ShapeBase):
             r = self._radius
             tau_segs = math.pi * 2 / self._segments
 
-            # Calcuate the outer points of the circle:
+            # Calculate the outer points of the circle:
             points = [(x + (r * math.cos(i * tau_segs)),
                        y + (r * math.sin(i * tau_segs))) for i in range(self._segments)]
 
-            # Create a list of trianges from the points:
+            # Create a list of triangles from the points:
             vertices = []
             for i, point in enumerate(points):
                 triangle = x, y, *points[i - 1], *point
@@ -433,6 +516,244 @@ class Circle(_ShapeBase):
         self._update_position()
 
 
+class Ellipse(_ShapeBase):
+    def __init__(self, x, y, a, b, color=(255, 255, 255), batch=None, group=None):
+        """Create an ellipse.
+
+        The ellipse's anchor point (x, y) defaults to the center of the ellipse.
+
+        :Parameters:
+            `x` : float
+                X coordinate of the ellipse.
+            `y` : float
+                Y coordinate of the ellipse.
+            `a` : float
+                Semi-major axes of the ellipse.
+            `b`: float
+                Semi-minor axes of the ellipse.
+            `color` : (int, int, int)
+                The RGB color of the ellipse. specify as a tuple of
+                three ints in the range of 0~255.
+            `batch` : `~pyglet.graphics.Batch`
+                Optional batch to add the circle to.
+            `group` : `~pyglet.graphics.Group`
+                Optional parent group of the circle.
+        """
+        self._x = x
+        self._y = y
+        self._a = a
+        self._b = b
+        self._rgb = color
+        self._rotation = 0
+        self._segments = int(max(a, b) / 1.25)
+        self._num_verts = self._segments * 2
+
+        self._batch = batch or Batch()
+        self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group)
+        self._vertex_list = self._batch.add(self._num_verts, GL_LINES, self._group, 'v2f', 'c4B')
+
+        self._update_position()
+        self._update_color()
+
+    def _update_position(self):
+        if not self._visible:
+            vertices = (0,) * self._num_verts * 4
+        else:
+            x = self._x + self._anchor_x
+            y = self._y + self._anchor_y
+            tau_segs = math.pi * 2 / self._segments
+
+            # Calculate the points of the ellipse by formula:
+            points = [(x + self._a * math.cos(i * tau_segs),
+                       y + self._b * math.sin(i * tau_segs)) for i in range(self._segments + 1)]
+
+            # Rotate all points:
+            if self._rotation:
+                points = _rotate(points, self._rotation, x, y)
+
+            # Create a list of lines from the points:
+            vertices = []
+            for i in range(len(points) - 1):
+                line_points = *points[i], *points[i + 1]
+                vertices.extend(line_points)
+        self._vertex_list.vertices[:] = vertices
+
+    def _update_color(self):
+        self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * self._num_verts
+
+    @property
+    def a(self):
+        """The semi-major axes of the ellipse.
+
+        :type: float
+        """
+        return self._a
+
+    @a.setter
+    def a(self, value):
+        self._a = value
+        self._update_position()
+
+    @property
+    def b(self):
+        """The semi-minor axes of the ellipse.
+
+        :type: float
+        """
+        return self._b
+
+    @b.setter
+    def b(self, value):
+        self._b = value
+        self._update_position()
+
+    @property
+    def rotation(self):
+        """Clockwise rotation of the arc, in degrees.
+
+        The arc will be rotated about its (anchor_x, anchor_y)
+        position.
+
+        :type: float
+        """
+        return self._rotation
+
+    @rotation.setter
+    def rotation(self, rotation):
+        self._rotation = rotation
+        self._update_position()
+
+    def draw(self):
+        """Draw the shape at its current position.
+
+        Using this method is not recommended. Instead, add the
+        shape to a `pyglet.graphics.Batch` for efficient rendering.
+        """
+        self._vertex_list.draw(GL_LINES)
+
+
+class Sector(_ShapeBase):
+    def __init__(self, x, y, radius, segments=None, angle=math.tau, start_angle=0,
+                 color=(255, 255, 255), batch=None, group=None):
+        """Create a sector of a circle.
+
+                The sector's anchor point (x, y) defaults to the center of the circle.
+
+                :Parameters:
+                    `x` : float
+                        X coordinate of the sector.
+                    `y` : float
+                        Y coordinate of the sector.
+                    `radius` : float
+                        The desired radius.
+                    `segments` : int
+                        You can optionally specify how many distinct triangles
+                        the sector should be made from. If not specified it will
+                        be automatically calculated using the formula:
+                        `max(14, int(radius / 1.25))`.
+                    `angle` : float
+                        The angle of the sector, in radians. Defaults to tau (pi * 2),
+                        which is a full circle.
+                    `start_angle` : float
+                        The start angle of the sector, in radians. Defaults to 0.
+                    `color` : (int, int, int)
+                        The RGB color of the sector, specified as a tuple of
+                        three ints in the range of 0-255.
+                    `batch` : `~pyglet.graphics.Batch`
+                        Optional batch to add the sector to.
+                    `group` : `~pyglet.graphics.Group`
+                        Optional parent group of the sector.
+                """
+        self._x = x
+        self._y = y
+        self._radius = radius
+        self._segments = segments or max(14, int(radius / 1.25))
+
+        self._rgb = color
+        self._angle = angle
+        self._start_angle = start_angle
+        self._rotation = 0
+
+        self._batch = batch or Batch()
+        self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group)
+
+        self._vertex_list = self._batch.add(self._segments * 3, GL_TRIANGLES, self._group, 'v2f', 'c4B')
+        self._update_position()
+        self._update_color()
+
+    def _update_position(self):
+        if not self._visible:
+            vertices = (0,) * self._segments * 6
+        else:
+            x = self._x + self._anchor_x
+            y = self._y + self._anchor_y
+            r = self._radius
+            tau_segs = self._angle / self._segments
+            start_angle = self._start_angle - math.radians(self._rotation)
+
+            # Calculate the outer points of the sector.
+            points = [(x + (r * math.cos((i * tau_segs) + start_angle)),
+                       y + (r * math.sin((i * tau_segs) + start_angle))) for i in range(self._segments + 1)]
+
+            # Create a list of triangles from the points
+            vertices = []
+            for i, point in enumerate(points[1:], start=1):
+                triangle = x, y, *points[i - 1], *point
+                vertices.extend(triangle)
+
+        self._vertex_list.vertices[:] = vertices
+
+    def _update_color(self):
+        self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * self._segments * 3
+
+    @property
+    def angle(self):
+        return self._angle
+
+    @angle.setter
+    def angle(self, angle):
+        self._angle = angle
+        self._update_position()
+
+    @property
+    def start_angle(self):
+        return self._start_angle
+
+    @start_angle.setter
+    def start_angle(self, angle):
+        self._start_angle = angle
+        self._update_position()
+
+    @property
+    def radius(self):
+        """The radius of the circle.
+
+        :type: float
+        """
+        return self._radius
+
+    @radius.setter
+    def radius(self, value):
+        self._radius = value
+        self._update_position()
+
+    @property
+    def rotation(self):
+        """Clockwise rotation of the sector, in degrees.
+
+        The sector will be rotated about its (anchor_x, anchor_y)
+        position.
+
+        :type: float
+        """
+        return self._rotation
+
+    @rotation.setter
+    def rotation(self, rotation):
+        self._rotation = rotation
+        self._update_position()
+
+
 class Line(_ShapeBase):
     def __init__(self, x, y, x2, y2, width=1, color=(255, 255, 255), batch=None, group=None):
         """Create a line.
@@ -553,7 +874,7 @@ class Rectangle(_ShapeBase):
     def __init__(self, x, y, width, height, color=(255, 255, 255), batch=None, group=None):
         """Create a rectangle or square.
 
-        The rectangles's anchor point defaults to the (x, y) coordinates,
+        The rectangle's anchor point defaults to the (x, y) coordinates,
         which are at the bottom left.
 
         :Parameters:
@@ -589,32 +910,20 @@ class Rectangle(_ShapeBase):
     def _update_position(self):
         if not self._visible:
             self._vertex_list.vertices = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
-        elif self._rotation:
-            x1 = -self._anchor_x
-            y1 = -self._anchor_y
-            x2 = x1 + self._width
-            y2 = y1 + self._height
-            x = self._x
-            y = self._y
-
-            r = -math.radians(self._rotation)
-            cr = math.cos(r)
-            sr = math.sin(r)
-            ax = x1 * cr - y1 * sr + x
-            ay = x1 * sr + y1 * cr + y
-            bx = x2 * cr - y1 * sr + x
-            by = x2 * sr + y1 * cr + y
-            cx = x2 * cr - y2 * sr + x
-            cy = x2 * sr + y2 * cr + y
-            dx = x1 * cr - y2 * sr + x
-            dy = x1 * sr + y2 * cr + y
-            self._vertex_list.vertices = (ax, ay, bx, by, cx, cy, ax, ay, cx, cy, dx, dy)
         else:
             x1 = self._x - self._anchor_x
             y1 = self._y - self._anchor_y
             x2 = x1 + self._width
             y2 = y1 + self._height
-            self._vertex_list.vertices = (x1, y1, x2, y1, x2, y2, x1, y1, x2, y2, x1, y2)
+            x = self._x
+            y = self._y
+
+            vertices = [(x1, y1), (x2, y1), (x2, y2), (x1, y1), (x2, y2), (x1, y2)]
+
+            if self._rotation:
+                vertices = _rotate(vertices, self._rotation, x, y)
+
+            self._vertex_list.vertices = tuple(value for vertex in vertices for value in vertex)
 
     def _update_color(self):
         self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * 6
@@ -667,7 +976,7 @@ class BorderedRectangle(_ShapeBase):
                  border_color=(100, 100, 100), batch=None, group=None):
         """Create a rectangle or square.
 
-        The rectangles's anchor point defaults to the (x, y) coordinates,
+        The rectangle's anchor point defaults to the (x, y) coordinates,
         which are at the bottom left.
 
         :Parameters:
@@ -679,9 +988,14 @@ class BorderedRectangle(_ShapeBase):
                 The width of the rectangle.
             `height` : float
                 The height of the rectangle.
+            `border` : float
+                The thickness of the border.
             `color` : (int, int, int)
                 The RGB color of the rectangle, specified as
                 a tuple of three ints in the range of 0-255.
+            `border_color` : (int, int, int)
+                The RGB color of the rectangle's border, specified as
+                a tuple of three ints in the range of 0-255.
             `batch` : `~pyglet.graphics.Batch`
                 Optional batch to add the rectangle to.
             `group` : `~pyglet.graphics.Group`
@@ -691,6 +1005,7 @@ class BorderedRectangle(_ShapeBase):
         self._y = y
         self._width = width
         self._height = height
+        self._rotation = 0
         self._border = border
         self._rgb = color
         self._brgb = border_color
@@ -707,16 +1022,26 @@ class BorderedRectangle(_ShapeBase):
             self._vertex_list.vertices = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
         else:
             b = self._border
-            bx1 = self._x - self._anchor_x
-            by1 = self._y - self._anchor_y
+            x = self._x
+            y = self._y
+
+            bx1 = x - self._anchor_x
+            by1 = y - self._anchor_y
             bx2 = bx1 + self._width
             by2 = by1 + self._height
             ix1 = bx1 + b
             iy1 = by1 + b
             ix2 = bx2 - b
             iy2 = by2 - b
-            self._vertex_list.vertices[:] = (ix1, iy1, ix2, iy1, ix2, iy2, ix1, iy2,
-                                             bx1, by1, bx2, by1, bx2, by2, bx1, by2,)
+
+            vertices = [(ix1, iy1), (ix2, iy1), (ix2, iy2), (ix1, iy2),
+                        (bx1, by1), (bx2, by1), (bx2, by2), (bx1, by2)]
+
+            if self._rotation:
+                vertices = _rotate(vertices, self._rotation, x, y)
+
+            # Flattening the list.
+            self._vertex_list.vertices[:] = tuple(value for vertex in vertices for value in vertex)
 
     def _update_color(self):
         opacity = int(self._opacity)
@@ -748,6 +1073,40 @@ class BorderedRectangle(_ShapeBase):
         self._height = value
         self._update_position()
 
+    @property
+    def rotation(self):
+        """Clockwise rotation of the rectangle, in degrees.
+
+        The Rectangle will be rotated about its (anchor_x, anchor_y)
+        position.
+
+        :type: float
+        """
+        return self._rotation
+
+    @rotation.setter
+    def rotation(self, value):
+        self._rotation = value
+        self._update_position()
+
+    @property
+    def border_color(self):
+        """The rectangle's border color.
+
+        This property sets the color of the border of a bordered rectangle.
+
+        The color is specified as an RGB tuple of integers '(red, green, blue)'.
+        Each color component must be in the range 0 (dark) to 255 (saturated).
+
+        :type: (int, int, int)
+        """
+        return self._brgb
+
+    @border_color.setter
+    def border_color(self, values):
+        self._brgb = tuple(map(int, values))
+        self._update_color()
+
 
 class Triangle(_ShapeBase):
     def __init__(self, x, y, x2, y2, x3, y3, color=(255, 255, 255), batch=None, group=None):
@@ -887,4 +1246,242 @@ class Triangle(_ShapeBase):
         self._update_position()
 
 
-__all__ = ('Arc', 'Circle', 'Line', 'Rectangle', 'BorderedRectangle', 'Triangle')
+class Star(_ShapeBase):
+    def __init__(self, x, y, outer_radius, inner_radius, num_spikes, rotation=0,
+                 color=(255, 255, 255), batch=None, group=None) -> None:
+        """Create a star.
+
+        The star's anchor point (x, y) defaults to the center of the star.
+
+        :Parameters:
+            `x` : float
+                The X coordinate of the star.
+            `y` : float
+                The Y coordinate of the star.
+            `outer_radius` : float
+                The desired outer radius of the star.
+            `inner_radius` : float
+                The desired inner radius of the star.
+            `num_spikes` : float
+                The desired number of spikes of the star.
+            `rotation` : float
+                The rotation of the star in degrees. A rotation of 0 degrees
+                will result in one spike lining up with the X axis in
+                positive direction.
+            `color` : (int, int, int)
+                The RGB color of the star, specified as
+                a tuple of three ints in the range of 0-255.
+            `batch` : `~pyglet.graphics.Batch`
+                Optional batch to add the star to.
+            `group` : `~pyglet.graphics.Group`
+                Optional parent group of the star.
+        """
+        self._x = x
+        self._y = y
+        self._outer_radius = outer_radius
+        self._inner_radius = inner_radius
+        self._num_spikes = num_spikes
+        self._rgb = color
+        self._rotation = rotation
+
+        self._batch = batch or Batch()
+        self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group)
+
+        self._vertex_list = self._batch.add(self._num_spikes*6, GL_TRIANGLES,
+                                            self._group, 'v2f', 'c4B')
+        self._update_position()
+        self._update_color()
+
+    def _update_position(self):
+        if not self._visible:
+            vertices = (0, 0) * self._num_spikes * 6
+        else:
+            x = self._x + self._anchor_x
+            y = self._y + self._anchor_y
+            r_i = self._inner_radius
+            r_o = self._outer_radius
+
+            # get angle covered by each line (= half a spike)
+            d_theta = math.pi / self._num_spikes
+
+            # phase shift rotation
+            phi = self._rotation / 180 * math.pi
+
+            # calculate alternating points on outer and outer circles
+            points = []
+            for i in range(self._num_spikes):
+                points.append((x + (r_o * math.cos(2*i * d_theta + phi)),
+                               y + (r_o * math.sin(2*i * d_theta + phi))))
+                points.append((x + (r_i * math.cos((2*i+1) * d_theta + phi)),
+                               y + (r_i * math.sin((2*i+1) * d_theta + phi))))
+
+            # create a list of doubled-up points from the points
+            vertices = []
+            for i, point in enumerate(points):
+                triangle = x, y, *points[i - 1], *point
+                vertices.extend(triangle)
+
+        self._vertex_list.vertices[:] = vertices
+
+    def _update_color(self):
+        self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * self._num_spikes * 6
+
+    @property
+    def outer_radius(self):
+        """The outer radius of the star."""
+        return self._outer_radius
+
+    @outer_radius.setter
+    def outer_radius(self, value):
+        self._outer_radius = value
+        self._update_position()
+
+    @property
+    def inner_radius(self):
+        """The inner radius of the star."""
+        return self._inner_radius
+
+    @inner_radius.setter
+    def inner_radius(self, value):
+        self._inner_radius = value
+        self._update_position()
+
+    @property
+    def num_spikes(self):
+        """Number of spikes of the star."""
+        return self._num_spikes
+
+    @num_spikes.setter
+    def num_spikes(self, value):
+        self._num_spikes = value
+        self._update_position()
+
+    @property
+    def rotation(self):
+        """Rotation of the star, in degrees.
+        """
+        return self._rotation
+
+    @rotation.setter
+    def rotation(self, rotation):
+        self._rotation = rotation
+        self._update_position()
+
+
+class Polygon(_ShapeBase):
+    def __init__(self, *coordinates, color=(255, 255, 255), batch=None, group=None):
+        """Create a convex polygon.
+
+        The polygon's anchor point defaults to the first vertex point.
+
+        :Parameters:
+            `coordinates` : List[[int, int]]
+                The coordinates for each point in the polygon.
+            `color` : (int, int, int)
+                The RGB color of the polygon, specified as
+                a tuple of three ints in the range of 0-255.
+            `batch` : `~pyglet.graphics.Batch`
+                Optional batch to add the polygon to.
+            `group` : `~pyglet.graphics.Group`
+                Optional parent group of the polygon.
+        """
+
+        # len(self._coordinates) = the number of vertices and sides in the shape.
+        self._coordinates = list(coordinates)
+
+        self._rotation = 0
+
+        self._rgb = color
+
+        self._batch = batch or Batch()
+        self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, group)
+        self._vertex_list = self._batch.add((len(self._coordinates) - 2) * 3, GL_TRIANGLES, self._group, 'v2f', 'c4B')
+        self._update_position()
+        self._update_color()
+
+    def _update_position(self):
+        if not self._visible:
+            self._vertex_list.vertices = tuple([0] * ((len(self._coordinates) - 2) * 6))
+        else:
+            # Adjust all coordinates by the anchor.
+            anchor_x = self._anchor_x
+            anchor_y = self._anchor_y
+            coords = [[x - anchor_x, y - anchor_y] for x, y in self._coordinates]
+
+            if self._rotation:
+                # Rotate the polygon around its first vertex.
+                x, y = self._coordinates[0]
+                coords = _rotate(coords, self._rotation, x, y)
+
+            # Triangulate the convex polygon.
+            triangles = []
+            for n in range(len(coords) - 2):
+                triangles += [coords[0], coords[n + 1], coords[n + 2]]
+
+            # Flattening the list before setting vertices to it.
+            self._vertex_list.vertices = tuple(value for coordinate in triangles for value in coordinate)
+
+    def _update_color(self):
+        self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * ((len(self._coordinates) - 2) * 3)
+
+    @property
+    def x(self):
+        """X coordinate of the shape.
+
+        :type: int or float
+        """
+        return self._coordinates[0][0]
+
+    @x.setter
+    def x(self, value):
+        self._coordinates[0][0] = value
+        self._update_position()
+
+    @property
+    def y(self):
+        """Y coordinate of the shape.
+
+        :type: int or float
+        """
+        return self._coordinates[0][1]
+
+    @y.setter
+    def y(self, value):
+        self._coordinates[0][1] = value
+        self._update_position()
+
+    @property
+    def position(self):
+        """The (x, y) coordinates of the shape, as a tuple.
+
+        :Parameters:
+            `x` : int or float
+                X coordinate of the shape.
+            `y` : int or float
+                Y coordinate of the shape.
+        """
+        return self._coordinates[0][0], self._coordinates[0][1]
+
+    @position.setter
+    def position(self, values):
+        self._coordinates[0][0], self._coordinates[0][1] = values
+        self._update_position()
+
+    @property
+    def rotation(self):
+        """Clockwise rotation of the polygon, in degrees.
+
+        The Polygon will be rotated about its (anchor_x, anchor_y)
+        position.
+
+        :type: float
+        """
+        return self._rotation
+
+    @rotation.setter
+    def rotation(self, rotation):
+        self._rotation = rotation
+        self._update_position()
+
+
+__all__ = ('Arc', 'Circle', 'Ellipse', 'Line', 'Rectangle', 'BorderedRectangle', 'Triangle', 'Star', 'Polygon', 'Sector')
diff --git a/pyglet/sprite.py b/pyglet/sprite.py
index 78be3f9..893798f 100644
--- a/pyglet/sprite.py
+++ b/pyglet/sprite.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -455,8 +455,8 @@ class Sprite(event.EventDispatcher):
         return self._x, self._y
 
     @position.setter
-    def position(self, pos):
-        self._x, self._y = pos
+    def position(self, position):
+        self._x, self._y = position
         self._update_position()
 
     @property
diff --git a/pyglet/text/__init__.py b/pyglet/text/__init__.py
index 258898f..190a7e6 100644
--- a/pyglet/text/__init__.py
+++ b/pyglet/text/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -303,6 +303,26 @@ class DocumentLabel(layout.TextLayout):
         self.document.set_style(0, len(self.document.text),
                                 {'color': color})
 
+    @property
+    def opacity(self):
+        """Blend opacity.
+
+        This property sets the alpha component of the colour of the label's
+        vertices.  With the default blend mode, this allows the layout to be
+        drawn with fractional opacity, blending with the background.
+
+        An opacity of 255 (the default) has no effect.  An opacity of 128 will
+        make the label appear semi-translucent.
+
+        :type: int
+        """
+        return self.color[3]
+
+    @opacity.setter
+    def opacity(self, alpha):
+        if alpha != self.color[3]:
+            self.color = list(map(int, (*self.color[:3], alpha)))
+
     @property
     def font_name(self):
         """Font family name.
@@ -392,7 +412,7 @@ class Label(DocumentLabel):
     """
 
     def __init__(self, text='',
-                 font_name=None, font_size=None, bold=False, italic=False,
+                 font_name=None, font_size=None, bold=False, italic=False, stretch=False,
                  color=(255, 255, 255, 255),
                  x=0, y=0, width=None, height=None,
                  anchor_x='left', anchor_y='baseline',
@@ -408,10 +428,12 @@ class Label(DocumentLabel):
                 first matching name is used.
             `font_size` : float
                 Font size, in points.
-            `bold` : bool
+            `bold` : bool/str
                 Bold font style.
-            `italic` : bool
+            `italic` : bool/str
                 Italic font style.
+            `stretch` : bool/str
+                 Stretch font style.
             `color` : (int, int, int, int)
                 Font colour, as RGBA components in range [0, 255].
             `x` : int
@@ -453,6 +475,7 @@ class Label(DocumentLabel):
             'font_size': font_size,
             'bold': bold,
             'italic': italic,
+            'stretch': stretch,
             'color': color,
             'align': align,
         })
@@ -521,3 +544,4 @@ class HTMLLabel(DocumentLabel):
     def text(self, text):
         self._text = text
         self.document = decode_html(text, self._location)
+
diff --git a/pyglet/text/caret.py b/pyglet/text/caret.py
index 111ad0e..ee75061 100644
--- a/pyglet/text/caret.py
+++ b/pyglet/text/caret.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -157,7 +157,7 @@ class Caret:
         clock.unschedule(self._blink)
         if visible and self._active and self.PERIOD:
             clock.schedule_interval(self._blink, self.PERIOD)
-            self._blink_visible = False # flipped immediately by next blink
+            self._blink_visible = False  # flipped immediately by next blink
         self._blink(0)
 
     def _get_visible(self):
@@ -388,8 +388,8 @@ class Caret:
         if update_ideal_x:
             self._ideal_x = x
 
-        x -= self._layout.top_group.translate_x
-        y -= self._layout.top_group.translate_y
+        x -= self._layout.top_group.view_x
+        y -= self._layout.top_group.view_y
         font = self._layout.document.get_font(max(0, self._position - 1))
         self._list.vertices[:] = [x, y + font.descent, x, y + font.ascent]
 
diff --git a/pyglet/text/document.py b/pyglet/text/document.py
index 7c312d3..c726171 100644
--- a/pyglet/text/document.py
+++ b/pyglet/text/document.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -40,7 +40,7 @@ Abstract representation
 
 Styled text in pyglet is represented by one of the :py:class:`~pyglet.text.document.AbstractDocument` classes,
 which manage the state representation of text and style independently of how
-it is loaded or rendered.  
+it is loaded or rendered.
 
 A document consists of the document text (a Unicode string) and a set of
 named style ranges.  For example, consider the following (artificial)
@@ -131,7 +131,7 @@ entire paragraph, otherwise results are undefined.
 ``align``
     ``left`` (default), ``center`` or ``right``.
 ``indent``
-    Additional horizontal space to insert before the first 
+    Additional horizontal space to insert before the first
 ``leading``
     Additional space to insert between consecutive lines within a paragraph,
     in points.  Defaults to 0.
@@ -159,7 +159,7 @@ document; it will be ignored by the built-in text classes.
 
 All style attributes (including those not present in a document) default to
 ``None`` (including the so-called "boolean" styles listed above).  The meaning
-of a ``None`` style is style- and application-dependent. 
+of a ``None`` style is style- and application-dependent.
 
 .. versionadded:: 1.1
 """
@@ -181,8 +181,8 @@ class InlineElement:
 
     Elements behave like a single glyph in the document.  They are
     measured by their horizontal advance, ascent above the baseline, and
-    descent below the baseline.  
-    
+    descent below the baseline.
+
     The pyglet layout classes reserve space in the layout for elements and
     call the element's methods to ensure they are rendered at the
     appropriate position.
@@ -225,8 +225,8 @@ class InlineElement:
 
         It is the responsibility of the element to clip itself against
         the layout boundaries, and position itself appropriately with respect
-        to the layout's position and viewport offset.  
-        
+        to the layout's position and viewport offset.
+
         The `TextLayout.top_state` graphics state implements this transform
         and clipping into window space.
 
@@ -263,7 +263,7 @@ class AbstractDocument(event.EventDispatcher):
     This class can be overridden to interface pyglet with a third-party
     document format.  It may be easier to implement the document format in
     terms of one of the supplied concrete classes :py:class:`~pyglet.text.document.FormattedDocument` or
-    :py:class:`~pyglet.text.document.UnformattedDocument`. 
+    :py:class:`~pyglet.text.document.UnformattedDocument`.
     """
     _previous_paragraph_re = re.compile(u'\n[^\n\u2029]*$')
     _next_paragraph_re = re.compile(u'[\n\u2029]')
@@ -469,8 +469,7 @@ class AbstractDocument(event.EventDispatcher):
                 inserted text.
 
         """
-        assert element._position is None, \
-            'Element is already in a document.'
+        assert element._position is None, 'Element is already in a document.'
         self.insert_text(position, '\0', attributes)
         element._position = position
         self._elements.append(element)
@@ -614,8 +613,9 @@ class UnformattedDocument(AbstractDocument):
         font_size = self.styles.get('font_size')
         bold = self.styles.get('bold', False)
         italic = self.styles.get('italic', False)
+        stretch = self.styles.get('stretch', False)
         return font.load(font_name, font_size,
-                         bold=bool(bold), italic=bool(italic), dpi=dpi)
+                         bold=bold, italic=italic, stretch=stretch, dpi=dpi)
 
     def get_element_runs(self):
         return runlist.ConstRunIterator(len(self._text), None)
@@ -659,11 +659,12 @@ class FormattedDocument(AbstractDocument):
             self.get_style_runs('font_size'),
             self.get_style_runs('bold'),
             self.get_style_runs('italic'),
+            self.get_style_runs('stretch'),
             dpi)
 
     def get_font(self, position, dpi=None):
-        iter = self.get_font_runs(dpi)
-        return iter[position]
+        runs_iter = self.get_font_runs(dpi)
+        return runs_iter[position]
 
     def get_element_runs(self):
         return _ElementIterator(self._elements, len(self._text))
@@ -680,8 +681,7 @@ class FormattedDocument(AbstractDocument):
                 try:
                     runs = self._style_runs[attribute]
                 except KeyError:
-                    runs = self._style_runs[attribute] = \
-                        runlist.RunList(0, None)
+                    runs = self._style_runs[attribute] = runlist.RunList(0, None)
                     runs.insert(0, len(self.text))
                 runs.set_run(start, start + len_text, value)
 
@@ -709,26 +709,21 @@ class _ElementIterator(runlist.RunIterator):
 
 class _FontStyleRunsRangeIterator:
     # XXX subclass runlist
-    def __init__(self, font_names, font_sizes, bolds, italics, dpi):
-        self.zip_iter = runlist.ZipRunIterator(
-            (font_names, font_sizes, bolds, italics))
+    def __init__(self, font_names, font_sizes, bolds, italics, stretch, dpi):
+        self.zip_iter = runlist.ZipRunIterator((font_names, font_sizes, bolds, italics, stretch))
         self.dpi = dpi
 
     def ranges(self, start, end):
         from pyglet import font
         for start, end, styles in self.zip_iter.ranges(start, end):
-            font_name, font_size, bold, italic = styles
-            ft = font.load(font_name, font_size,
-                           bold=bool(bold), italic=bool(italic),
-                           dpi=self.dpi)
+            font_name, font_size, bold, italic, stretch = styles
+            ft = font.load(font_name, font_size, bold=bool(bold), italic=bool(italic), stretch=stretch, dpi=self.dpi)
             yield start, end, ft
 
     def __getitem__(self, index):
         from pyglet import font
-        font_name, font_size, bold, italic = self.zip_iter[index]
-        return font.load(font_name, font_size,
-                         bold=bool(bold), italic=bool(italic),
-                         dpi=self.dpi)
+        font_name, font_size, bold, italic, stretch = self.zip_iter[index]
+        return font.load(font_name, font_size, bold=bool(bold), italic=bool(italic), stretch=stretch, dpi=self.dpi)
 
 
 class _NoStyleRangeIterator:
diff --git a/pyglet/text/formats/__init__.py b/pyglet/text/formats/__init__.py
index d8a20cf..90cf8d7 100644
--- a/pyglet/text/formats/__init__.py
+++ b/pyglet/text/formats/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/text/formats/attributed.py b/pyglet/text/formats/attributed.py
index 3eb815c..b730cec 100644
--- a/pyglet/text/formats/attributed.py
+++ b/pyglet/text/formats/attributed.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/text/formats/html.py b/pyglet/text/formats/html.py
index 2e1e069..2f6443f 100644
--- a/pyglet/text/formats/html.py
+++ b/pyglet/text/formats/html.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -123,6 +123,8 @@ class HTMLDecoder(HTMLParser, structured.StructuredTextDecoder):
         'font_name': 'Times New Roman',
         'font_size': 12,
         'margin_bottom': '12pt',
+        'bold': False,
+        'italic': False,
     }
 
     #: Map HTML font sizes to actual font sizes, in points.
diff --git a/pyglet/text/formats/plaintext.py b/pyglet/text/formats/plaintext.py
index e5b4a32..356be70 100644
--- a/pyglet/text/formats/plaintext.py
+++ b/pyglet/text/formats/plaintext.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/text/formats/structured.py b/pyglet/text/formats/structured.py
index 119af12..705d52c 100644
--- a/pyglet/text/formats/structured.py
+++ b/pyglet/text/formats/structured.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/text/layout.py b/pyglet/text/layout.py
index e0d27d4..4ecbacd 100644
--- a/pyglet/text/layout.py
+++ b/pyglet/text/layout.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -178,7 +178,7 @@ def _parse_distance(distance, dpi):
         return int(distance)
 
     match = _distance_re.match(distance)
-    assert match, 'Could not parse distance %s' % (distance)
+    assert match, 'Could not parse distance %s' % distance
     if not match:
         return 0
 
@@ -245,9 +245,7 @@ class _LayoutContext:
     def __init__(self, layout, document, colors_iter, background_iter):
         self.colors_iter = colors_iter
         underline_iter = document.get_style_runs('underline')
-        self.decoration_iter = runlist.ZipRunIterator(
-            (background_iter,
-             underline_iter))
+        self.decoration_iter = runlist.ZipRunIterator((background_iter, underline_iter))
         self.baseline_iter = runlist.FilteredRunIterator(
             document.get_style_runs('baseline'),
             lambda value: value is not None, 0)
@@ -255,8 +253,7 @@ class _LayoutContext:
 
 class _StaticLayoutContext(_LayoutContext):
     def __init__(self, layout, document, colors_iter, background_iter):
-        super(_StaticLayoutContext, self).__init__(layout, document,
-                                                   colors_iter, background_iter)
+        super().__init__(layout, document, colors_iter, background_iter)
         self.vertex_lists = layout._vertex_lists
         self.boxes = layout._boxes
 
@@ -286,7 +283,7 @@ class _AbstractBox:
         self.advance = advance
         self.length = length
 
-    def place(self, layout, i, x, y):
+    def place(self, layout, i, x, y, context):
         raise NotImplementedError('abstract')
 
     def delete(self, layout):
@@ -316,8 +313,7 @@ class _GlyphBox(_AbstractBox):
                 and kerns in the glyph list.
 
         """
-        super(_GlyphBox, self).__init__(
-            font.ascent, font.descent, advance, len(glyphs))
+        super().__init__(font.ascent, font.descent, advance, len(glyphs))
         assert owner
         self.owner = owner
         self.font = font
@@ -329,8 +325,7 @@ class _GlyphBox(_AbstractBox):
         try:
             group = layout.groups[self.owner]
         except KeyError:
-            group = layout.groups[self.owner] = \
-                TextLayoutTextureGroup(self.owner, layout.foreground_group)
+            group = layout.groups[self.owner] = TextLayoutTextureGroup(self.owner, layout.foreground_group)
 
         n_glyphs = self.length
         vertices = []
@@ -384,13 +379,11 @@ class _GlyphBox(_AbstractBox):
                 x2 += glyph.advance + kern
 
             if bg is not None:
-                background_vertices.extend(
-                    [x1, y1, x2, y1, x2, y2, x1, y2])
+                background_vertices.extend([x1, y1, x2, y1, x2, y2, x1, y2])
                 background_colors.extend(bg * 4)
 
             if underline is not None:
-                underline_vertices.extend(
-                    [x1, y + baseline - 2, x2, y + baseline - 2])
+                underline_vertices.extend([x1, y + baseline - 2, x2, y + baseline - 2])
                 underline_colors.extend(underline * 2)
 
             x1 = x2
@@ -442,8 +435,7 @@ class _InlineElementBox(_AbstractBox):
     def __init__(self, element):
         """Create a glyph run holding a single element.
         """
-        super(_InlineElementBox, self).__init__(
-            element.ascent, element.descent, element.advance, 1)
+        super().__init__(element.ascent, element.descent, element.advance, 1)
         self.element = element
         self.placed = False
 
@@ -551,6 +543,43 @@ class TextLayoutGroup(graphics.Group):
 class ScrollableTextLayoutGroup(graphics.Group):
     """Top-level rendering group for :py:class:`~pyglet.text.layout.ScrollableTextLayout`.
 
+    The group maintains internal state for setting the clipping planes and
+    view transform for scrolling.  Because the group has internal state
+    specific to the text layout, the group is never shared.
+    """
+    x = 0
+    y = 0
+    width = 0
+    height = 0
+    view_x = 0
+    view_y = 0
+
+    def set_state(self):
+        glPushAttrib(GL_ENABLE_BIT | GL_TRANSFORM_BIT | GL_CURRENT_BIT)
+
+        glEnable(GL_BLEND)
+        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+
+        glEnable(GL_SCISSOR_TEST)
+        glScissor(self.x, self.y, self.width, self.height)
+
+        glTranslatef(-self.view_x, -self.view_y, 0)
+
+    def unset_state(self):
+        glTranslatef(self.view_x, self.view_y, 0)
+        glDisable(GL_SCISSOR_TEST)
+        glPopAttrib()
+
+    def __eq__(self, other):
+        return self is other
+
+    def __hash__(self):
+        return id(self)
+
+
+class IncrementalTextLayoutGroup(graphics.Group):
+    """Top-level rendering group for :py:class:`~pyglet.text.layout.IncrementalTextLayout`.
+
     The group maintains internal state for setting the clipping planes and
     view transform for scrolling.  Because the group has internal state
     specific to the text layout, the group is never shared.
@@ -566,86 +595,97 @@ class ScrollableTextLayoutGroup(graphics.Group):
 
     def set_state(self):
         glPushAttrib(GL_ENABLE_BIT | GL_TRANSFORM_BIT | GL_CURRENT_BIT)
+
         glEnable(GL_BLEND)
         glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
-        # Disable clipping planes to check culling.
-        glEnable(GL_CLIP_PLANE0)
-        glEnable(GL_CLIP_PLANE1)
-        glEnable(GL_CLIP_PLANE2)
-        glEnable(GL_CLIP_PLANE3)
-        # Left
-        glClipPlane(GL_CLIP_PLANE0, (GLdouble * 4)(1, 0, 0, -(self._clip_x - 1)))
-        # Top
-        glClipPlane(GL_CLIP_PLANE1, (GLdouble * 4)(0, -1, 0, self._clip_y))
-        # Right
-        glClipPlane(GL_CLIP_PLANE2, (GLdouble * 4)(-1, 0, 0, self._clip_x + self._clip_width + 1))
-        # Bottom
-        glClipPlane(GL_CLIP_PLANE3, (GLdouble * 4)(0, 1, 0, -(self._clip_y - self._clip_height)))
+
+        glEnable(GL_SCISSOR_TEST)
+        glScissor(self._clip_x, self._clip_y - self._clip_height, self._clip_width, self._clip_height)
+
         glTranslatef(self.translate_x, self.translate_y, 0)
 
     def unset_state(self):
         glTranslatef(-self.translate_x, -self.translate_y, 0)
+        glDisable(GL_SCISSOR_TEST)
         glPopAttrib()
 
-    def _set_top(self, top):
+    @property
+    def top(self):
+        return self._clip_y
+
+    @top.setter
+    def top(self, top):
+        """Top edge of the text layout (measured from the
+        bottom of the graphics viewport).
+
+        :type: int
+        """
         self._clip_y = top
         self.translate_y = self._clip_y - self._view_y
 
-    top = property(lambda self: self._clip_y, _set_top,
-                   doc="""Top edge of the text layout (measured from the
-    bottom of the graphics viewport).
+    @property
+    def left(self):
+        return self._clip_x
 
-    :type: int
-    """)
+    @left.setter
+    def left(self, left):
+        """Left edge of the text layout.
 
-    def _set_left(self, left):
+        :type: int
+        """
         self._clip_x = left
         self.translate_x = self._clip_x - self._view_x
 
-    left = property(lambda self: self._clip_x, _set_left,
-                    doc="""Left edge of the text layout.
+    @property
+    def width(self):
+        return self._clip_width
 
-    :type: int
-    """)
+    @width.setter
+    def width(self, width):
+        """Width of the text layout.
 
-    def _set_width(self, width):
+        :type: int
+        """
         self._clip_width = width
 
-    width = property(lambda self: self._clip_width, _set_width,
-                     doc="""Width of the text layout.
+    @property
+    def height(self):
+        return self._clip_height
 
-    :type: int
-    """)
+    @height.setter
+    def height(self, height):
+        """Height of the text layout.
 
-    def _set_height(self, height):
+        :type: int
+        """
         self._clip_height = height
 
-    height = property(lambda self: self._height, _set_height,
-                      doc="""Height of the text layout.
+    @property
+    def view_x(self):
+        return self._view_x
 
-    :type: int
-    """)
+    @view_x.setter
+    def view_x(self, view_x):
+        """Horizontal scroll offset.
 
-    def _set_view_x(self, view_x):
+        :type: int
+        """
         self._view_x = view_x
         self.translate_x = self._clip_x - self._view_x
 
-    view_x = property(lambda self: self._view_x, _set_view_x,
-                      doc="""Horizontal scroll offset.
+    @property
+    def view_y(self):
+        return self._view_y
 
-    :type: int
-    """)
+    @view_y.setter
+    def view_y(self, view_y):
+        """Vertical scroll offset.
 
-    def _set_view_y(self, view_y):
+        :type: int
+        """
         self._view_y = view_y
         self.translate_y = self._clip_y - self._view_y
 
-    view_y = property(lambda self: self._view_y, _set_view_y,
-                      doc="""Vertical scroll offset.
-
-    :type: int
-    """)
-
     def __eq__(self, other):
         return self is other
 
@@ -687,7 +727,7 @@ class TextLayoutTextureGroup(graphics.Group):
 
     def __init__(self, texture, parent):
         assert texture.target == GL_TEXTURE_2D
-        super(TextLayoutTextureGroup, self).__init__(parent)
+        super().__init__(parent)
 
         self.texture = texture
 
@@ -706,9 +746,7 @@ class TextLayoutTextureGroup(graphics.Group):
                 self.parent == other.parent)
 
     def __repr__(self):
-        return '%s(%d, %r)' % (self.__class__.__name__,
-                               self.texture.id,
-                               self.parent)
+        return '%s(%d, %r)' % (self.__class__.__name__, self.texture.id, self.parent)
 
 
 class TextLayout:
@@ -749,13 +787,22 @@ class TextLayout:
     top_group = TextLayoutGroup()
     background_group = graphics.OrderedGroup(0, top_group)
     foreground_group = TextLayoutForegroundGroup(1, top_group)
-    foreground_decoration_group = \
-        TextLayoutForegroundDecorationGroup(2, top_group)
+    foreground_decoration_group = TextLayoutForegroundDecorationGroup(2, top_group)
 
     _update_enabled = True
     _own_batch = False
     _origin_layout = False  # Lay out relative to origin?  Otherwise to box.
 
+    _x = 0
+    _y = 0
+    _width = None
+    _height = None
+    _anchor_x = 'left'
+    _anchor_y = 'bottom'
+    _content_valign = 'top'
+    _multiline = False
+    _visible = True
+
     def __init__(self, document, width=None, height=None,
                  multiline=False, dpi=None, batch=None, group=None,
                  wrap_lines=True):
@@ -810,6 +857,35 @@ class TextLayout:
         self._dpi = dpi
         self.document = document
 
+    @property
+    def dpi(self):
+        """Get DPI used by this layout.
+
+        :type: float
+        """
+        return self._dpi
+
+    @property
+    def document(self):
+        """Document to display.
+
+         For :py:class:`~pyglet.text.layout.IncrementalTextLayout` it is
+         far more efficient to modify a document in-place than to replace
+         the document instance on the layout.
+
+         :type: `AbstractDocument`
+         """
+        return self._document
+
+    @document.setter
+    def document(self, document):
+        if self._document:
+            self._document.remove_handlers(self)
+            self._uninit_document()
+        document.push_handlers(self)
+        self._document = document
+        self._init_document()
+
     @property
     def batch(self):
         """The Batch that this Layout is assigned to.
@@ -836,6 +912,250 @@ class TextLayout:
             self._own_batch = False
             self._update()
 
+    @property
+    def x(self):
+        """X coordinate of the layout.
+
+        See also :py:attr:`~pyglet.text.layout.TextLayout.anchor_x`.
+
+        :type: int
+        """
+        return self._x
+
+    @x.setter
+    def x(self, x):
+        self._set_x(x)
+
+    def _set_x(self, x):
+        if self._boxes:
+            self._x = x
+            self._update()
+        else:
+            dx = x - self._x
+            for vertex_list in self._vertex_lists:
+                vertices = vertex_list.vertices[:]
+                vertices[::2] = [x + dx for x in vertices[::2]]
+                vertex_list.vertices[:] = vertices
+            self._x = x
+
+    @property
+    def y(self):
+        """Y coordinate of the layout.
+
+        See also `anchor_y`.
+
+        :type: int
+        """
+        return self._y
+
+    @y.setter
+    def y(self, y):
+        self._set_y(y)
+
+    def _set_y(self, y):
+        if self._boxes:
+            self._y = y
+            self._update()
+        else:
+            dy = y - self._y
+            for vertex_list in self._vertex_lists:
+                vertices = vertex_list.vertices[:]
+                vertices[1::2] = [y + dy for y in vertices[1::2]]
+                vertex_list.vertices[:] = vertices
+            self._y = y
+
+    @property
+    def position(self):
+        """The (X, Y) coordinates of the layout, as a tuple.
+
+        See also :py:attr:`~pyglet.text.layout.TextLayout.anchor_x`,
+        and :py:attr:`~pyglet.text.layout.TextLayout.anchor_y`.
+
+        :type: (int, int)
+        """
+        return self._x, self._y
+
+    @position.setter
+    def position(self, position):
+        self.update(*position)
+
+    def update(self, x, y):
+        """Change both X and Y positions of the layout at once for faster performance.
+
+        :Parameters:
+            `x` : int
+                X coordinate of the layout.
+            `y` : int
+                Y coordinate of the layout.
+        """
+        if self._boxes:
+            self._x = x
+            self._y = y
+            self._update()
+        else:
+            dx = x - self._x
+            dy = y - self._y
+            for vertex_list in self._vertex_lists:
+                vertices = vertex_list.vertices[:]
+                vertices[::2] = [x + dx for x in vertices[::2]]
+                vertices[1::2] = [y + dy for y in vertices[1::2]]
+                vertex_list.vertices[:] = vertices
+            self._x = x
+            self._y = y
+
+    @property
+    def visible(self):
+        """True if the layout will be drawn.
+
+        :type: bool
+        """
+        return self._visible
+
+    @visible.setter
+    def visible(self, value):
+        if value != self._visible:
+            self._visible = value
+
+            if value:
+                self._update()
+            else:
+                self.delete()
+
+    @property
+    def width(self):
+        """Width of the layout.
+
+        This property has no effect if `multiline` is False or `wrap_lines` is False.
+
+        :type: int
+        """
+        return self._width
+
+    @width.setter
+    def width(self, width):
+        self._width = width
+        self._wrap_lines_invariant()
+        self._update()
+
+    @property
+    def height(self):
+        """Height of the layout.
+
+        :type: int
+        """
+        return self._height
+
+    @height.setter
+    def height(self, height):
+        self._height = height
+        self._update()
+
+    @property
+    def multiline(self):
+        """Set if multiline layout is enabled.
+
+        If multiline is False, newline and paragraph characters are ignored and
+        text is not word-wrapped.
+        If True, the text is word-wrapped only if the `wrap_lines` is True.
+
+        :type: bool
+        """
+        return self._multiline
+
+    @multiline.setter
+    def multiline(self, multiline):
+        self._multiline = multiline
+        self._wrap_lines_invariant()
+        self._update()
+
+    @property
+    def anchor_x(self):
+        """Horizontal anchor alignment.
+
+        This property determines the meaning of the `x` coordinate.
+        It is one of the enumerants:
+
+        ``"left"`` (default)
+            The X coordinate gives the position of the left edge of the layout.
+        ``"center"``
+            The X coordinate gives the position of the center of the layout.
+        ``"right"``
+            The X coordinate gives the position of the right edge of the layout.
+
+        For the purposes of calculating the position resulting from this
+        alignment, the width of the layout is taken to be `width` if `multiline`
+        is True and `wrap_lines` is True, otherwise `content_width`.
+
+        :type: str
+        """
+        return self._anchor_x
+
+    @anchor_x.setter
+    def anchor_x(self, anchor_x):
+        self._anchor_x = anchor_x
+        self._update()
+
+    @property
+    def anchor_y(self):
+        """Vertical anchor alignment.
+
+        This property determines the meaning of the `y` coordinate.
+        It is one of the enumerants:
+
+        ``"top"``
+            The Y coordinate gives the position of the top edge of the layout.
+        ``"center"``
+            The Y coordinate gives the position of the center of the layout.
+        ``"baseline"``
+            The Y coordinate gives the position of the baseline of the first
+            line of text in the layout.
+        ``"bottom"`` (default)
+            The Y coordinate gives the position of the bottom edge of the layout.
+
+        For the purposes of calculating the position resulting from this
+        alignment, the height of the layout is taken to be the smaller of
+        `height` and `content_height`.
+
+        See also `content_valign`.
+
+        :type: str
+        """
+        return self._anchor_y
+
+    @anchor_y.setter
+    def anchor_y(self, anchor_y):
+        self._anchor_y = anchor_y
+        self._update()
+
+    @property
+    def content_valign(self):
+        """Vertical alignment of content within larger layout box.
+
+        This property determines how content is positioned within the layout
+        box when ``content_height`` is less than ``height``.  It is one
+        of the enumerants:
+
+        ``top`` (default)
+            Content is aligned to the top of the layout box.
+        ``center``
+            Content is centered vertically within the layout box.
+        ``bottom``
+            Content is aligned to the bottom of the layout box.
+
+        This property has no effect when ``content_height`` is greater
+        than ``height`` (in which case the content is aligned to the top) or when
+        ``height`` is ``None`` (in which case there is no vertical layout box
+        dimension).
+
+        :type: str
+        """
+        return self._content_valign
+
+    @content_valign.setter
+    def content_valign(self, content_valign):
+        self._content_valign = content_valign
+        self._update()
+
     def _wrap_lines_invariant(self):
         self._wrap_lines = self._multiline and self._wrap_lines_flag
         assert not self._wrap_lines or self._width, \
@@ -869,14 +1189,6 @@ class TextLayout:
         self._update_enabled = True
         self._update()
 
-    dpi = property(lambda self: self._dpi,
-                   doc="""Get DPI used by this layout.
-
-    Read-only.
-
-    :type: float
-    """)
-
     def delete(self):
         """Remove this layout from its batch.
         """
@@ -904,37 +1216,15 @@ class TextLayout:
             self.top_group = TextLayoutGroup(group)
             self.background_group = graphics.OrderedGroup(0, self.top_group)
             self.foreground_group = TextLayoutForegroundGroup(1, self.top_group)
-            self.foreground_decoration_group = \
-                TextLayoutForegroundDecorationGroup(2, self.top_group)
+            self.foreground_decoration_group = TextLayoutForegroundDecorationGroup(2, self.top_group)
             # Otherwise class groups are (re)used.
 
-    def _get_document(self):
-        return self._document
-
-    def _set_document(self, document):
-        if self._document:
-            self._document.remove_handlers(self)
-            self._uninit_document()
-        document.push_handlers(self)
-        self._document = document
-        self._init_document()
-
-    document = property(_get_document, _set_document,
-                        doc="""Document to display.
- 
-     For :py:class:`~pyglet.text.layout.IncrementalTextLayout` it is far more efficient to modify a document
-     in-place than to replace the document instance on the layout.
- 
-     :type: `AbstractDocument`
-     """)
-
     def _get_lines(self):
         len_text = len(self._document.text)
         glyphs = self._get_glyphs()
         owner_runs = runlist.RunList(len_text, None)
         self._get_owner_runs(owner_runs, glyphs, 0, len_text)
-        lines = [line for line in self._flow_glyphs(glyphs, owner_runs,
-                                                    0, len_text)]
+        lines = [line for line in self._flow_glyphs(glyphs, owner_runs, 0, len_text)]
         self.content_width = 0
         self._flow_lines(lines, 0, len(lines))
         return lines
@@ -965,11 +1255,9 @@ class TextLayout:
             left = self._get_left()
             top = self._get_top(lines)
 
-        context = _StaticLayoutContext(self, self._document,
-                                       colors_iter, background_iter)
+        context = _StaticLayoutContext(self, self._document, colors_iter, background_iter)
         for line in lines:
-            self._create_vertex_lists(left + line.x, top + line.y,
-                                      line.start, line.boxes, context)
+            self._create_vertex_lists(left + line.x, top + line.y, line.start, line.boxes, context)
 
     def _update_color(self):
         colors_iter = self._document.get_style_runs('color')
@@ -997,7 +1285,7 @@ class TextLayout:
         elif self._anchor_x == 'right':
             return self._x - width
         else:
-            assert False, 'Invalid anchor_x'
+            assert False, '`anchor_x` must be either "left", "center", or "right".'
 
     def _get_top(self, lines):
         if self._height is None:
@@ -1012,7 +1300,7 @@ class TextLayout:
             elif self._content_valign == 'center':
                 offset = max(0, self._height - self.content_height) // 2
             else:
-                assert False, 'Invalid content_valign'
+                assert False, '`content_valign` must be either "top", "bottom", or "center".'
 
         if self._anchor_y == 'top':
             return self._y - offset
@@ -1028,7 +1316,21 @@ class TextLayout:
             else:
                 return self._y + height // 2 - offset
         else:
-            assert False, 'Invalid anchor_y'
+            assert False, '`anchor_y` must be either "top", "bottom", "center", or "baseline".'
+
+    def _get_bottom(self, lines):
+        height = self._height or self.content_height
+
+        if self._anchor_y == 'top':
+            return self._y - height
+        elif self._anchor_y == 'bottom':
+            return self._y
+        elif self._anchor_y == 'center':
+            return self._y - height // 2
+        elif self._anchor_y == 'baseline':
+            return self._y - height + lines[0].ascent
+        else:
+            assert False, '`anchor_y` must be either "top", "bottom", "center", or "baseline".'
 
     def _init_document(self):
         self._update()
@@ -1042,7 +1344,12 @@ class TextLayout:
         The event handler is bound by the text layout; there is no need for
         applications to interact with this method.
         """
-        self._init_document()
+        if self._visible:
+            self._init_document()
+        else:
+            if self.document.text:
+                # Update content width and height, since text may change while hidden.
+                self._get_lines()
 
     def on_delete_text(self, start, end):
         """Event handler for `AbstractDocument.on_delete_text`.
@@ -1050,7 +1357,8 @@ class TextLayout:
         The event handler is bound by the text layout; there is no need for
         applications to interact with this method.
         """
-        self._init_document()
+        if self._visible:
+            self._init_document()
 
     def on_style_text(self, start, end, attributes):
         """Event handler for `AbstractDocument.on_style_text`.
@@ -1182,8 +1490,7 @@ class TextLayout:
             # Iterate over glyphs in this owner run.  `text` is the
             # corresponding character data for the glyph, and is used to find
             # whitespace and newlines.
-            for (text, glyph) in zip(self.document.text[start:end],
-                                     glyphs[start:end]):
+            for (text, glyph) in zip(self.document.text[start:end], glyphs[start:end]):
                 if nokern:
                     kern = 0
                     nokern = False
@@ -1198,8 +1505,7 @@ class TextLayout:
                     run_accum_width = 0
 
                     if text == '\t':
-                        # Fix up kern for this glyph to align to the next tab
-                        # stop
+                        # Fix up kern for this glyph to align to the next tab stop
                         for tab_stop in tab_stops_iterator[index]:
                             tab_stop = self._parse_distance(tab_stop)
                             if tab_stop > x + line.margin_left:
@@ -1207,15 +1513,12 @@ class TextLayout:
                         else:
                             # No more tab stops, tab to 100 pixels
                             tab = 50.
-                            tab_stop = \
-                                (((x + line.margin_left) // tab) + 1) * tab
-                        kern = int(tab_stop - x - line.margin_left -
-                                   glyph.advance)
+                            tab_stop = (((x + line.margin_left) // tab) + 1) * tab
+                        kern = int(tab_stop - x - line.margin_left - glyph.advance)
 
                     owner_accum.append((kern, glyph))
                     owner_accum_commit.extend(owner_accum)
-                    owner_accum_commit_width += owner_accum_width + \
-                                                glyph.advance + kern
+                    owner_accum_commit_width += owner_accum_width + glyph.advance + kern
                     eol_ws += glyph.advance + kern
 
                     owner_accum = []
@@ -1231,8 +1534,7 @@ class TextLayout:
                 else:
                     new_paragraph = text in u'\n\u2029'
                     new_line = (text == u'\u2028') or new_paragraph
-                    if (wrap and self._wrap_lines and x + kern + glyph.advance >= width)\
-                            or new_line:
+                    if (wrap and self._wrap_lines and x + kern + glyph.advance >= width) or new_line:
                         # Either the pending runs have overflowed the allowed
                         # line width or a newline was encountered.  Either
                         # way, the current line must be flushed.
@@ -1368,6 +1670,13 @@ class TextLayout:
         line = _Line(start)
         font = font_iterator[0]
 
+        if self._width:
+            align_iterator = runlist.FilteredRunIterator(
+                self._document.get_style_runs('align'),
+                lambda value: value in ('left', 'right', 'center'),
+                'left')
+            line.align = align_iterator[start]
+
         for start, end, owner in owner_iterator:
             font = font_iterator[start]
             width = 0
@@ -1408,10 +1717,8 @@ class TextLayout:
             y = 0
         else:
             line = lines[start - 1]
-            line_spacing = \
-                self._parse_distance(line_spacing_iterator[line.start])
-            leading = \
-                self._parse_distance(leading_iterator[line.start])
+            line_spacing = self._parse_distance(line_spacing_iterator[line.start])
+            leading = self._parse_distance(leading_iterator[line.start])
 
             y = line.y
             if line_spacing is None:
@@ -1423,8 +1730,7 @@ class TextLayout:
         for line in lines[start:]:
             if line.paragraph_begin:
                 y -= self._parse_distance(margin_top_iterator[line.start])
-                line_spacing = \
-                    self._parse_distance(line_spacing_iterator[line.start])
+                line_spacing = self._parse_distance(line_spacing_iterator[line.start])
                 leading = self._parse_distance(leading_iterator[line.start])
             else:
                 y -= leading
@@ -1436,13 +1742,11 @@ class TextLayout:
             if line.align == 'left' or line.width > self.width:
                 line.x = line.margin_left
             elif line.align == 'center':
-                line.x = (self.width - line.margin_left - line.margin_right
-                          - line.width) // 2 + line.margin_left
+                line.x = (self.width - line.margin_left - line.margin_right - line.width) // 2 + line.margin_left
             elif line.align == 'right':
                 line.x = self.width - line.margin_right - line.width
 
-            self.content_width = max(self.content_width,
-                                     line.width + line.margin_left)
+            self.content_width = max(self.content_width, line.width + line.margin_left)
 
             if line.y == y and line_index >= end:
                 # Early exit: all invalidated lines have been reflowed and the
@@ -1468,217 +1772,20 @@ class TextLayout:
             x += box.advance
             i += box.length
 
-    _x = 0
-
-    def _set_x(self, x):
-        if self._boxes:
-            self._x = x
-            self._update()
-        else:
-            dx = x - self._x
-            for vertex_list in self._vertex_lists:
-                vertex_list.vertices[::2] = [v + dx for v in vertex_list.vertices[::2]]
-            self._x = x
-
-    def _get_x(self):
-        return self._x
-
-    x = property(_get_x, _set_x,
-                 doc="""X coordinate of the layout.
-
-    See also :py:attr:`~pyglet.text.layout.TextLayout.anchor_x`.
-
-    :type: int
-    """)
-
-    _y = 0
-
-    def _set_y(self, y):
-        if self._boxes:
-            self._y = y
-            self._update()
-        else:
-            dy = y - self._y
-            for vertex_list in self._vertex_lists:
-                vertex_list.vertices[1::2] = [v + dy for v in vertex_list.vertices[1::2]]
-            self._y = y
-
-    def _get_y(self):
-        return self._y
-
-    y = property(_get_y, _set_y,
-                 doc="""Y coordinate of the layout.
-
-    See also `anchor_y`.
-
-    :type: int
-    """)
-
-    _width = None
-
-    def _set_width(self, width):
-        self._width = width
-        self._wrap_lines_invariant()
-        self._update()
-
-    def _get_width(self):
-        return self._width
-
-    width = property(_get_width, _set_width,
-                     doc="""Width of the layout.
-
-    This property has no effect if `multiline` is False or `wrap_lines` is False.
-
-    :type: int
-    """)
-
-    _height = None
-
-    def _set_height(self, height):
-        self._height = height
-        self._update()
-
-    def _get_height(self):
-        return self._height
-
-    height = property(_get_height, _set_height,
-                      doc="""Height of the layout.
-
-    :type: int
-    """)
-
-    _multiline = False
-
-    def _set_multiline(self, multiline):
-        self._multiline = multiline
-        self._wrap_lines_invariant()
-        self._update()
-
-    def _get_multiline(self):
-        return self._multiline
-
-    multiline = property(_get_multiline, _set_multiline,
-                         doc="""Set if multiline layout is enabled.
-
-    If multiline is False, newline and paragraph characters are ignored and
-    text is not word-wrapped.
-    If True, the text is word-wrapped only if the `wrap_lines` is True.
-
-    :type: bool
-    """)
-
-    _anchor_x = 'left'
-
-    def _set_anchor_x(self, anchor_x):
-        self._anchor_x = anchor_x
-        self._update()
-
-    def _get_anchor_x(self):
-        return self._anchor_x
-
-    anchor_x = property(_get_anchor_x, _set_anchor_x,
-                        doc="""Horizontal anchor alignment.
-
-    This property determines the meaning of the `x` coordinate.  It is one of
-    the enumerants:
-
-    ``"left"`` (default)
-        The X coordinate gives the position of the left edge of the layout.
-    ``"center"``
-        The X coordinate gives the position of the center of the layout.
-    ``"right"``
-        The X coordinate gives the position of the right edge of the layout.
-
-    For the purposes of calculating the position resulting from this
-    alignment, the width of the layout is taken to be `width` if `multiline`
-    is True and `wrap_lines` is True, otherwise `content_width`.
-
-    :type: str
-    """)
-
-    _anchor_y = 'bottom'
-
-    def _set_anchor_y(self, anchor_y):
-        self._anchor_y = anchor_y
-        self._update()
-
-    def _get_anchor_y(self):
-        return self._anchor_y
-
-    anchor_y = property(_get_anchor_y, _set_anchor_y,
-                        doc="""Vertical anchor alignment.
-
-    This property determines the meaning of the `y` coordinate.  It is one of
-    the enumerants:
-
-    ``"top"``
-        The Y coordinate gives the position of the top edge of the layout.
-    ``"center"``
-        The Y coordinate gives the position of the center of the layout.
-    ``"baseline"``
-        The Y coordinate gives the position of the baseline of the first
-        line of text in the layout.
-    ``"bottom"`` (default)
-        The Y coordinate gives the position of the bottom edge of the layout.
-
-    For the purposes of calculating the position resulting from this
-    alignment, the height of the layout is taken to be the smaller of
-    `height` and `content_height`.
-
-    See also `content_valign`.
-
-    :type: str
-    """)
-
-    _content_valign = 'top'
-
-    def _set_content_valign(self, content_valign):
-        self._content_valign = content_valign
-        self._update()
-
-    def _get_content_valign(self):
-        return self._content_valign
-
-    content_valign = property(_get_content_valign, _set_content_valign,
-                              doc="""Vertical alignment of content within
-    larger layout box.
-
-    This property determines how content is positioned within the layout
-    box when ``content_height`` is less than ``height``.  It is one
-    of the enumerants:
-
-    ``top`` (default)
-        Content is aligned to the top of the layout box.
-    ``center``
-        Content is centered vertically within the layout box.
-    ``bottom``
-        Content is aligned to the bottom of the layout box.
-
-    This property has no effect when ``content_height`` is greater
-    than ``height`` (in which case the content is aligned to the top) or when
-    ``height`` is ``None`` (in which case there is no vertical layout box
-    dimension).
-
-    :type: str
-    """)
-
 
 class ScrollableTextLayout(TextLayout):
     """Display text in a scrollable viewport.
 
     This class does not display a scrollbar or handle scroll events; it merely
-    clips the text that would be drawn in :py:func:`~pyglet.text.layout.TextLayout` to the bounds of the
-    layout given by `x`, `y`, `width` and `height`; and offsets the text by a
-    scroll offset.
+    clips the text that would be drawn in :py:func:`~pyglet.text.layout.TextLayout`
+    to the bounds of the layout given by `x`, `y`, `width` and `height`;
+    and offsets the text by a scroll offset.
 
     Use `view_x` and `view_y` to scroll the text within the viewport.
     """
-    _origin_layout = True
 
-    def __init__(self, document, width, height, multiline=False, dpi=None,
-                 batch=None, group=None, wrap_lines=True):
-        super(ScrollableTextLayout, self).__init__(
-            document, width, height, multiline, dpi, batch, group, wrap_lines)
+    def __init__(self, document, width, height, multiline=False, dpi=None, batch=None, group=None, wrap_lines=True):
+        super().__init__(document, width, height, multiline, dpi, batch, group, wrap_lines)
         self.top_group.width = self._width
         self.top_group.height = self._height
 
@@ -1687,113 +1794,105 @@ class ScrollableTextLayout(TextLayout):
         self.top_group = ScrollableTextLayoutGroup(group)
         self.background_group = graphics.OrderedGroup(0, self.top_group)
         self.foreground_group = TextLayoutForegroundGroup(1, self.top_group)
-        self.foreground_decoration_group = \
-            TextLayoutForegroundDecorationGroup(2, self.top_group)
+        self.foreground_decoration_group = TextLayoutForegroundDecorationGroup(2, self.top_group)
 
-    def _set_x(self, x):
-        self._x = x
-        self.top_group.left = self._get_left()
+    # Properties
 
-    def _get_x(self):
+    @property
+    def x(self):
         return self._x
 
-    x = property(_get_x, _set_x)
+    @x.setter
+    def x(self, x):
+        super()._set_x(x)
+        self.top_group.x = self._get_left()
 
-    def _set_y(self, y):
-        self._y = y
-        self.top_group.top = self._get_top(self._get_lines())
-
-    def _get_y(self):
+    @property
+    def y(self):
         return self._y
 
-    y = property(_get_y, _set_y)
-
-    def _set_width(self, width):
-        super(ScrollableTextLayout, self)._set_width(width)
-        self.top_group.left = self._get_left()
-        self.top_group.width = self._width
-
-    def _get_width(self):
-        return self._width
+    @y.setter
+    def y(self, y):
+        super()._set_y(y)
+        self.top_group.y = y
 
-    width = property(_get_width, _set_width)
-
-    def _set_height(self, height):
-        super(ScrollableTextLayout, self)._set_height(height)
-        self.top_group.top = self._get_top(self._get_lines())
-        self.top_group.height = self._height
+    @property
+    def position(self):
+        return self._x, self._y
 
-    def _get_height(self):
-        return self._height
+    @position.setter
+    def position(self, position):
+        self.x, self.y = position
 
-    height = property(_get_height, _set_height)
+    @property
+    def anchor_x(self):
+        return self._anchor_x
 
-    def _set_anchor_x(self, anchor_x):
+    @anchor_x.setter
+    def anchor_x(self, anchor_x):
         self._anchor_x = anchor_x
-        self.top_group.left = self._get_left()
-
-    def _get_anchor_x(self):
-        return self._anchor_x
+        super()._update()
+        self.top_group.x = self._get_left()
 
-    anchor_x = property(_get_anchor_x, _set_anchor_x)
+    @property
+    def anchor_y(self):
+        return self._anchor_y
 
-    def _set_anchor_y(self, anchor_y):
+    @anchor_y.setter
+    def anchor_y(self, anchor_y):
         self._anchor_y = anchor_y
-        self.top_group.top = self._get_top(self._get_lines())
+        super()._update()
+        self.top_group.y = self._get_bottom(self._get_lines())
 
-    def _get_anchor_y(self):
-        return self._anchor_y
+    # Offset of content within viewport
 
-    anchor_y = property(_get_anchor_y, _set_anchor_y)
+    @property
+    def view_x(self):
+        """Horizontal scroll offset.
 
-    # Offset of content within viewport
+        The initial value is 0, and the left edge of the text will touch the left
+        side of the layout bounds.  A positive value causes the text to "scroll"
+        to the right.  Values are automatically clipped into the range
+        ``[0, content_width - width]``
 
-    def _set_view_x(self, view_x):
+        :type: int
+        """
+        return self.top_group.view_x
+
+    @view_x.setter
+    def view_x(self, view_x):
         view_x = max(0, min(self.content_width - self.width, view_x))
         self.top_group.view_x = view_x
 
-    def _get_view_x(self):
-        return self.top_group.view_x
+    @property
+    def view_y(self):
+        """Vertical scroll offset.
 
-    view_x = property(_get_view_x, _set_view_x,
-                      doc="""Horizontal scroll offset.
+        The initial value is 0, and the top of the text will touch the top of the
+        layout bounds (unless the content height is less than the layout height,
+        in which case `content_valign` is used).
 
-    The initial value is 0, and the left edge of the text will touch the left
-    side of the layout bounds.  A positive value causes the text to "scroll"
-    to the right.  Values are automatically clipped into the range
-    ``[0, content_width - width]``
+        A negative value causes the text to "scroll" upwards.  Values outside of
+        the range ``[height - content_height, 0]`` are automatically clipped in
+        range.
 
-    :type: int
-    """)
+        :type: int
+        """
+        return self.top_group.view_y
 
-    def _set_view_y(self, view_y):
+    @view_y.setter
+    def view_y(self, view_y):
         # view_y must be negative.
         view_y = min(0, max(self.height - self.content_height, view_y))
         self.top_group.view_y = view_y
 
-    def _get_view_y(self):
-        return self.top_group.view_y
-
-    view_y = property(_get_view_y, _set_view_y,
-                      doc="""Vertical scroll offset.
-
-    The initial value is 0, and the top of the text will touch the top of the
-    layout bounds (unless the content height is less than the layout height,
-    in which case `content_valign` is used).
-
-    A negative value causes the text to "scroll" upwards.  Values outside of
-    the range ``[height - content_height, 0]`` are automatically clipped in
-    range.
 
-    :type: int
-    """)
-
-
-class IncrementalTextLayout(ScrollableTextLayout, event.EventDispatcher):
+class IncrementalTextLayout(TextLayout, event.EventDispatcher):
     """Displayed text suitable for interactive editing and/or scrolling
     large documents.
 
-    Unlike :py:func:`~pyglet.text.layout.TextLayout` and :py:class:`~pyglet.text.layout.ScrollableTextLayout`, this class generates
+    Unlike :py:func:`~pyglet.text.layout.TextLayout` and
+    :py:class:`~pyglet.text.layout.ScrollableTextLayout`, this class generates
     vertex lists only for lines of text that are visible.  As the document is
     scrolled, vertex lists are deleted and created as appropriate to keep
     video memory usage to a minimum and improve rendering speed.
@@ -1812,9 +1911,10 @@ class IncrementalTextLayout(ScrollableTextLayout, event.EventDispatcher):
     _selection_color = [255, 255, 255, 255]
     _selection_background_color = [46, 106, 197, 255]
 
-    def __init__(self, document, width, height, multiline=False, dpi=None,
-                 batch=None, group=None, wrap_lines=True):
-        event.EventDispatcher.__init__(self)
+    _origin_layout = False
+
+    def __init__(self, document, width, height, multiline=False, dpi=None, batch=None, group=None, wrap_lines=True):
+
         self.glyphs = []
         self.lines = []
 
@@ -1827,18 +1927,22 @@ class IncrementalTextLayout(ScrollableTextLayout, event.EventDispatcher):
 
         self.owner_runs = runlist.RunList(0, None)
 
-        ScrollableTextLayout.__init__(self,
-                                      document, width, height, multiline, dpi, batch, group,
-                                      wrap_lines)
+        super().__init__(document, width, height, multiline, dpi, batch, group, wrap_lines)
 
         self.top_group.width = width
         self.top_group.left = self._get_left()
         self.top_group.height = height
         self.top_group.top = self._get_top(self._get_lines())
 
+    def _init_groups(self, group):
+        # Scrollable layout never shares group becauase of translation.
+        self.top_group = IncrementalTextLayoutGroup(group)
+        self.background_group = graphics.OrderedGroup(0, self.top_group)
+        self.foreground_group = TextLayoutForegroundGroup(1, self.top_group)
+        self.foreground_decoration_group = TextLayoutForegroundDecorationGroup(2, self.top_group)
+
     def _init_document(self):
-        assert self._document, \
-            'Cannot remove document from IncrementalTextLayout'
+        assert self._document, 'Cannot remove document from IncrementalTextLayout'
         self.on_insert_text(0, self._document.text)
 
     def _uninit_document(self):
@@ -1893,16 +1997,12 @@ class IncrementalTextLayout(ScrollableTextLayout, event.EventDispatcher):
         self._update()
 
     def on_style_text(self, start, end, attributes):
-        if ('font_name' in attributes or
-                    'font_size' in attributes or
-                    'bold' in attributes or
-                    'italic' in attributes):
+        if 'font_name' in attributes or 'font_size' in attributes or 'bold' in attributes or 'italic' in attributes:
             self.invalid_glyphs.invalidate(start, end)
-        elif False:  # Attributes that change flow
-            self.invalid_flow.invalidate(start, end)
-        elif ('color' in attributes or
-                      'background_color' in attributes):
+        elif 'color' in attributes or 'background_color' in attributes:
             self.invalid_style.invalidate(start, end)
+        else:  # Attributes that change flow
+            self.invalid_flow.invalidate(start, end)
 
         self._update()
 
@@ -1933,8 +2033,7 @@ class IncrementalTextLayout(ScrollableTextLayout, event.EventDispatcher):
         self._update_vertex_lists()
         self.top_group.top = self._get_top(self.lines)
 
-        # Reclamp view_y in case content height has changed and reset top of
-        # content.
+        # Reclamp view_y in case content height has changed and reset top of content.
         self.view_y = self.view_y
         self.top_group.top = self._get_top(self._get_lines())
 
@@ -1964,8 +2063,7 @@ class IncrementalTextLayout(ScrollableTextLayout, event.EventDispatcher):
         runs = runlist.ZipRunIterator((
             self._document.get_font_runs(dpi=self._dpi),
             self._document.get_element_runs()))
-        for start, end, (font, element) in \
-                runs.ranges(invalid_start, invalid_end):
+        for start, end, (font, element) in runs.ranges(invalid_start, invalid_end):
             if element:
                 self.glyphs[start] = _InlineElementBox(element)
             else:
@@ -1973,8 +2071,7 @@ class IncrementalTextLayout(ScrollableTextLayout, event.EventDispatcher):
                 self.glyphs[start:end] = font.get_glyphs(text)
 
         # Update owner runs
-        self._get_owner_runs(
-            self.owner_runs, self.glyphs, invalid_start, invalid_end)
+        self._get_owner_runs(self.owner_runs, self.glyphs, invalid_start, invalid_end)
 
         # Updated glyphs need flowing
         self.invalid_flow.invalidate(invalid_start, invalid_end)
@@ -1994,8 +2091,8 @@ class IncrementalTextLayout(ScrollableTextLayout, event.EventDispatcher):
 
         # Flow from previous line; fixes issue with adding a space into
         # overlong line (glyphs before space would then flow back onto
-        # previous line).  TODO Could optimise this by keeping track of where
-        # the overlong lines are.
+        # previous line).
+        # TODO:  Could optimise this by keeping track of where the overlong lines are.
         line_index = max(0, line_index - 1)
 
         # (No need to find last invalid line; the update loop below stops
@@ -2017,15 +2114,13 @@ class IncrementalTextLayout(ScrollableTextLayout, event.EventDispatcher):
         content_width_invalid = False
         next_start = invalid_start
 
-        for line in self._flow_glyphs(self.glyphs, self.owner_runs,
-                                      invalid_start, len(self._document.text)):
+        for line in self._flow_glyphs(self.glyphs, self.owner_runs, invalid_start, len(self._document.text)):
             try:
                 old_line = self.lines[line_index]
                 old_line.delete(self)
                 old_line_width = old_line.width + old_line.margin_left
                 new_line_width = line.width + line.margin_left
-                if (old_line_width == self.content_width and
-                            new_line_width < old_line_width):
+                if old_line_width == self.content_width and new_line_width < old_line_width:
                     content_width_invalid = True
                 self.lines[line_index] = line
                 self.invalid_lines.invalidate(line_index, line_index + 1)
@@ -2058,8 +2153,7 @@ class IncrementalTextLayout(ScrollableTextLayout, event.EventDispatcher):
             # Rescan all lines to look for the new maximum content width
             content_width = 0
             for line in self.lines:
-                content_width = max(line.width + line.margin_left,
-                                    content_width)
+                content_width = max(line.width + line.margin_left, content_width)
             self.content_width = content_width
 
     def _update_flow_lines(self):
@@ -2119,8 +2213,7 @@ class IncrementalTextLayout(ScrollableTextLayout, event.EventDispatcher):
                 self._selection_end,
                 self._selection_background_color)
 
-        context = _IncrementalLayoutContext(self, self._document,
-                                            colors_iter, background_iter)
+        context = _IncrementalLayoutContext(self, self._document, colors_iter, background_iter)
 
         for line in self.lines[invalid_start:invalid_end]:
             line.delete(self)
@@ -2133,59 +2226,137 @@ class IncrementalTextLayout(ScrollableTextLayout, event.EventDispatcher):
             elif y + line.ascent < self.view_y - self.height:
                 break
 
-            self._create_vertex_lists(line.x, y, line.start,
-                                      line.boxes, context)
+            self._create_vertex_lists(line.x, y, line.start, line.boxes, context)
 
-    # Invalidate everything when width changes
+    @property
+    def x(self):
+        return self._x
 
-    def _set_width(self, width):
-        if width == self._width:
-            return
+    @x.setter
+    def x(self, x):
+        self._x = x
+        self.top_group.left = self._get_left()
 
-        self.invalid_flow.invalidate(0, len(self.document.text))
-        super(IncrementalTextLayout, self)._set_width(width)
+    @property
+    def y(self):
+        return self._y
+
+    @y.setter
+    def y(self, y):
+        self._y = y
+        self.top_group.top = self._get_top(self._get_lines())
+
+    @property
+    def position(self):
+        return self._x, self._y
+
+    @position.setter
+    def position(self, position):
+        self.x, self.y = position
+
+    @property
+    def anchor_x(self):
+        return self._anchor_x
+
+    @anchor_x.setter
+    def anchor_x(self, anchor_x):
+        self._anchor_x = anchor_x
+        self.top_group.left = self._get_left()
+
+    @property
+    def anchor_y(self):
+        return self._anchor_y
+
+    @anchor_y.setter
+    def anchor_y(self, anchor_y):
+        self._anchor_y = anchor_y
+        self.top_group.top = self._get_top(self._get_lines())
 
-    def _get_width(self):
+    @property
+    def width(self):
         return self._width
 
-    width = property(_get_width, _set_width)
+    @width.setter
+    def width(self, width):
+        # Invalidate everything when width changes
+        if width == self._width:
+            return
+        self._width = width
+        super()._update()
+        self.invalid_flow.invalidate(0, len(self.document.text))
+        self.top_group.left = self._get_left()
+        self.top_group.width = self._width
+
+    @property
+    def height(self):
+        return self._height
 
-    # Recalculate visible lines when height changes
-    def _set_height(self, height):
+    @height.setter
+    def height(self, height):
+        # Recalculate visible lines when height changes
         if height == self._height:
             return
-
-        super(IncrementalTextLayout, self)._set_height(height)
+        self._height = height
+        super()._update()
+        self.top_group.top = self._get_top(self._get_lines())
+        self.top_group.height = self._height
         if self._update_enabled:
             self._update_visible_lines()
             self._update_vertex_lists()
 
-    def _get_height(self):
-        return self._height
-
-    height = property(_get_height, _set_height)
+    @property
+    def multiline(self):
+        return self._multiline
 
-    def _set_multiline(self, multiline):
+    @multiline.setter
+    def multiline(self, multiline):
         self.invalid_flow.invalidate(0, len(self.document.text))
-        super(IncrementalTextLayout, self)._set_multiline(multiline)
+        self._multiline = multiline
+        self._wrap_lines_invariant()
+        self._update()
 
-    def _get_multiline(self):
-        return self._multiline
+    @property
+    def view_x(self):
+        """Horizontal scroll offset.
 
-    multiline = property(_get_multiline, _set_multiline)
+        The initial value is 0, and the left edge of the text will touch the left
+        side of the layout bounds.  A positive value causes the text to "scroll"
+        to the right.  Values are automatically clipped into the range
+        ``[0, content_width - width]``
 
-    # Invalidate invisible/visible lines when y scrolls
+        :type: int
+        """
+        return self.top_group.view_x
 
-    def _set_view_y(self, view_y):
-        # view_y must be negative.
-        super(IncrementalTextLayout, self)._set_view_y(view_y)
-        self._update_visible_lines()
-        self._update_vertex_lists()
+    @view_x.setter
+    def view_x(self, view_x):
+        view_x = max(0, min(self.content_width - self.width, view_x))
+        self.top_group.view_x = view_x
+
+    @property
+    def view_y(self):
+        """Vertical scroll offset.
 
-    def _get_view_y(self):
+        The initial value is 0, and the top of the text will touch the top of the
+        layout bounds (unless the content height is less than the layout height,
+        in which case `content_valign` is used).
+
+        A negative value causes the text to "scroll" upwards.  Values outside of
+        the range ``[height - content_height, 0]`` are automatically clipped in
+        range.
+
+        :type: int
+        """
         return self.top_group.view_y
 
-    view_y = property(_get_view_y, _set_view_y)
+    @view_y.setter
+    def view_y(self, view_y):
+        # Invalidate invisible/visible lines when y scrolls
+        # view_y must be negative.
+        view_y = min(0, max(self.height - self.content_height, view_y))
+        self.top_group.view_y = view_y
+        self._update_visible_lines()
+        self._update_vertex_lists()
 
     # Visible selection
 
@@ -2223,59 +2394,63 @@ class IncrementalTextLayout(ScrollableTextLayout, event.EventDispatcher):
 
         self._update()
 
-    selection_start = property(
-        lambda self: self._selection_start,
-        lambda self, v: self.set_selection(v, self._selection_end),
-        doc="""Starting position of the active selection.
+    @property
+    def selection_start(self):
+        """Starting position of the active selection.
+
+        :see: `set_selection`
 
-    :see: `set_selection`
+        :type: int
+        """
+        return self._selection_start
+
+    @selection_start.setter
+    def selection_start(self, start):
+        self.set_selection(start, self._selection_end)
 
-    :type: int
-    """)
+    @property
+    def selection_end(self):
+        """End position of the active selection (exclusive).
 
-    selection_end = property(
-        lambda self: self._selection_end,
-        lambda self, v: self.set_selection(self._selection_start, v),
-        doc="""End position of the active selection (exclusive).
+        :see: `set_selection`
 
-    :see: `set_selection`
+        :type: int
+        """
+        return self._selection_end
+
+    @selection_end.setter
+    def selection_end(self, end):
+        self.set_selection(self._selection_start, end)
+
+    @property
+    def selection_color(self):
+        """Text color of active selection.
 
-    :type: int
-    """)
+        The color is an RGBA tuple with components in range [0, 255].
 
-    def _get_selection_color(self):
+        :type: (int, int, int, int)
+        """
         return self._selection_color
 
-    def _set_selection_color(self, color):
+    @selection_color.setter
+    def selection_color(self, color):
         self._selection_color = color
-        self.invalid_style.invalidate(self._selection_start,
-                                      self._selection_end)
-
-    selection_color = property(_get_selection_color, _set_selection_color,
-                               doc="""Text color of active selection.
+        self.invalid_style.invalidate(self._selection_start, self._selection_end)
 
-    The color is an RGBA tuple with components in range [0, 255].
+    @property
+    def selection_background_color(self):
+        """Background color of active selection.
 
-    :type: (int, int, int, int)
-    """)
+        The color is an RGBA tuple with components in range [0, 255].
 
-    def _get_selection_background_color(self):
+        :type: (int, int, int, int)
+        """
         return self._selection_background_color
 
-    def _set_selection_background_color(self, background_color):
+    @selection_background_color.setter
+    def selection_background_color(self, background_color):
         self._selection_background_color = background_color
-        self.invalid_style.invalidate(self._selection_start,
-                                      self._selection_end)
-
-    selection_background_color = property(_get_selection_background_color,
-                                          _set_selection_background_color,
-                                          doc="""Background color of active
-    selection.
-
-    The color is an RGBA tuple with components in range [0, 255].
-
-    :type: (int, int, int, int)
-    """)
+        self.invalid_style.invalidate(self._selection_start, self._selection_end)
 
     # Coordinate translation
 
@@ -2335,8 +2510,7 @@ class IncrementalTextLayout(ScrollableTextLayout, event.EventDispatcher):
             position -= box.length
             x += box.advance
 
-        return (x + self.top_group.translate_x,
-                line.y + self.top_group.translate_y + baseline)
+        return x + self.top_group.view_x, line.y + self.top_group.view_y + baseline
 
     def get_line_from_point(self, x, y):
         """Get the closest line index to a point.
@@ -2372,8 +2546,7 @@ class IncrementalTextLayout(ScrollableTextLayout, event.EventDispatcher):
         :return: (x, y)
         """
         line = self.lines[line]
-        return (line.x + self.top_group.translate_x,
-                line.y + self.top_group.translate_y)
+        return line.x + self.top_group.translate_x, line.y + self.top_group.translate_y
 
     def get_line_from_position(self, position):
         """Get the line index of a character position in the document.
@@ -2468,8 +2641,7 @@ class IncrementalTextLayout(ScrollableTextLayout, event.EventDispatcher):
             self.view_x = x - 10
         elif x >= self.view_x + self.width:
             self.view_x = x - self.width + 10
-        elif (x >= self.view_x + self.width - 10 and
-                      self.content_width > self.width):
+        elif x >= self.view_x + self.width - 10 and self.content_width > self.width:
             self.view_x = x - self.width + 10
 
     if _is_pyglet_doc_run:
diff --git a/pyglet/text/runlist.py b/pyglet/text/runlist.py
index b08f1a3..4a55776 100644
--- a/pyglet/text/runlist.py
+++ b/pyglet/text/runlist.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/util.py b/pyglet/util.py
index 4877b77..e66f1c4 100644
--- a/pyglet/util.py
+++ b/pyglet/util.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/window/__init__.py b/pyglet/window/__init__.py
index 30f58c5..0bbfb07 100644
--- a/pyglet/window/__init__.py
+++ b/pyglet/window/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -125,6 +125,7 @@ above, "Working with multiple screens")::
 
 import sys
 import math
+import warnings
 
 import pyglet
 import pyglet.window.key
@@ -392,6 +393,10 @@ class BaseWindow(with_metaclass(_WindowMetaclass, EventDispatcher)):
     WINDOW_STYLE_TOOL = 'tool'
     #: A window style without any decoration.
     WINDOW_STYLE_BORDERLESS = 'borderless'
+    #: A window style for transparent, interactable windows
+    WINDOW_STYLE_TRANSPARENT = 'transparent'
+    #: A window style for transparent, topmost, click-through-able overlays
+    WINDOW_STYLE_OVERLAY = 'overlay'
 
     #: The default mouse cursor.
     CURSOR_DEFAULT = None
@@ -599,6 +604,10 @@ class BaseWindow(with_metaclass(_WindowMetaclass, EventDispatcher)):
             if not config:
                 raise NoSuchConfigException('No standard config is available.')
 
+        # Necessary on Windows. More investigation needed:
+        if style in ('transparent', 'overlay'):
+            config.alpha = 8
+
         if not config.is_complete():
             config = screen.get_best_config(config)
 
@@ -608,6 +617,7 @@ class BaseWindow(with_metaclass(_WindowMetaclass, EventDispatcher)):
         # Set these in reverse order to above, to ensure we get user preference
         self._context = context
         self._config = self._context.config
+
         # XXX deprecate config's being screen-specific
         if hasattr(self._config, 'screen'):
             self._screen = self._config.screen
@@ -647,6 +657,15 @@ class BaseWindow(with_metaclass(_WindowMetaclass, EventDispatcher)):
         app.windows.add(self)
         self._create()
 
+        # Raise a warning if an OpenGL 2.0 context is not available. This is a common case
+        # with virtual machines, or on Windows without fully supported GPU drivers.
+        gl_info = context.get_info()
+        if not gl_info.have_version(2, 0):
+            message = ("\nYour graphics drivers do not support OpenGL 2.0.\n"
+                       "You may experience rendering issues or crashes.\n"
+                       f"{gl_info.get_vendor()}\n{gl_info.get_renderer()}\n{gl_info.get_version()}")
+            warnings.warn(message)
+
         self.switch_to()
         if visible:
             self.set_visible(True)
@@ -1884,7 +1903,9 @@ if _is_pyglet_doc_run:
     del BaseWindow
 else:
     # Try to determine which platform to use.
-    if pyglet.compat_platform == 'darwin':
+    if pyglet.options['headless']:
+        from pyglet.window.headless import HeadlessWindow as Window
+    elif pyglet.compat_platform == 'darwin':
         from pyglet.window.cocoa import CocoaWindow as Window
     elif pyglet.compat_platform in ('win32', 'cygwin'):
         from pyglet.window.win32 import Win32Window as Window
diff --git a/pyglet/window/cocoa/__init__.py b/pyglet/window/cocoa/__init__.py
index edbf9fa..714110a 100644
--- a/pyglet/window/cocoa/__init__.py
+++ b/pyglet/window/cocoa/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -619,3 +619,6 @@ class CocoaWindow(BaseWindow):
 
         NSApp = NSApplication.sharedApplication()
         NSApp.setPresentationOptions_(options)
+
+
+__all__ = ["CocoaWindow"]
diff --git a/pyglet/window/cocoa/pyglet_delegate.py b/pyglet/window/cocoa/pyglet_delegate.py
index c8c836a..9575fca 100644
--- a/pyglet/window/cocoa/pyglet_delegate.py
+++ b/pyglet/window/cocoa/pyglet_delegate.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/window/cocoa/pyglet_textview.py b/pyglet/window/cocoa/pyglet_textview.py
index c0dc971..4c32b60 100644
--- a/pyglet/window/cocoa/pyglet_textview.py
+++ b/pyglet/window/cocoa/pyglet_textview.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/window/cocoa/pyglet_view.py b/pyglet/window/cocoa/pyglet_view.py
index fabb5a2..06dfd4c 100644
--- a/pyglet/window/cocoa/pyglet_view.py
+++ b/pyglet/window/cocoa/pyglet_view.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/window/cocoa/pyglet_window.py b/pyglet/window/cocoa/pyglet_window.py
index 1c18e89..9f29660 100644
--- a/pyglet/window/cocoa/pyglet_window.py
+++ b/pyglet/window/cocoa/pyglet_window.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/window/cocoa/systemcursor.py b/pyglet/window/cocoa/systemcursor.py
index e183d18..89fd22a 100644
--- a/pyglet/window/cocoa/systemcursor.py
+++ b/pyglet/window/cocoa/systemcursor.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/window/event.py b/pyglet/window/event.py
index 37be8fd..6951cb2 100644
--- a/pyglet/window/event.py
+++ b/pyglet/window/event.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/window/headless/__init__.py b/pyglet/window/headless/__init__.py
new file mode 100644
index 0000000..bc8ef3d
--- /dev/null
+++ b/pyglet/window/headless/__init__.py
@@ -0,0 +1,145 @@
+# ----------------------------------------------------------------------------
+# pyglet
+# Copyright (c) 2006-2008 Alex Holkner
+# Copyright (c) 2008-2022 pyglet contributors
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+#  * Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+#  * Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in
+#    the documentation and/or other materials provided with the
+#    distribution.
+#  * Neither the name of pyglet nor the names of its
+#    contributors may be used to endorse or promote products
+#    derived from this software without specific prior written
+#    permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# ----------------------------------------------------------------------------
+
+from pyglet.window import BaseWindow, _PlatformEventHandler, _ViewEventHandler
+from pyglet.window import WindowException, NoSuchDisplayException, MouseCursorException
+from pyglet.window import MouseCursor, DefaultMouseCursor, ImageMouseCursor
+
+
+from pyglet.libs.egl import egl
+
+
+from pyglet.canvas.headless import HeadlessCanvas
+
+# from pyglet.window import key
+# from pyglet.window import mouse
+from pyglet.event import EventDispatcher
+
+# Platform event data is single item, so use platform event handler directly.
+HeadlessEventHandler = _PlatformEventHandler
+ViewEventHandler = _ViewEventHandler
+
+
+class HeadlessWindow(BaseWindow):
+    _egl_display_connection = None
+    _egl_surface = None
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+    def _recreate(self, changes):
+        pass
+
+    def flip(self):
+        if self.context:
+            self.context.flip()
+
+    def switch_to(self):
+        if self.context:
+            self.context.set_current()
+
+    def set_caption(self, caption):
+        pass
+
+    def set_minimum_size(self, width, height):
+        pass
+
+    def set_maximum_size(self, width, height):
+        pass
+
+    def set_size(self, width, height):
+        pass
+
+    def get_size(self):
+        return self._width, self._height
+
+    def set_location(self, x, y):
+        pass
+
+    def get_location(self):
+        pass
+
+    def activate(self):
+        pass
+
+    def set_visible(self, visible=True):
+        pass
+
+    def minimize(self):
+        pass
+
+    def maximize(self):
+        pass
+
+    def set_vsync(self, vsync):
+        pass
+
+    def set_mouse_platform_visible(self, platform_visible=None):
+        pass
+
+    def set_exclusive_mouse(self, exclusive=True):
+        pass
+
+    def set_exclusive_keyboard(self, exclusive=True):
+        pass
+
+    def get_system_mouse_cursor(self, name):
+        pass
+
+    def dispatch_events(self):
+        while self._event_queue:
+            EventDispatcher.dispatch_event(self, *self._event_queue.pop(0))
+
+    def dispatch_pending_events(self):
+        pass
+
+    def _create(self):
+        self._egl_display_connection = self.display._display_connection
+
+        if not self._egl_surface:
+            pbuffer_attribs = (egl.EGL_WIDTH, self._width, egl.EGL_HEIGHT, self._height, egl.EGL_NONE)
+            pbuffer_attrib_array = (egl.EGLint * len(pbuffer_attribs))(*pbuffer_attribs)
+            self._egl_surface = egl.eglCreatePbufferSurface(self._egl_display_connection,
+                                                            self.config._egl_config,
+                                                            pbuffer_attrib_array)
+
+            self.canvas = HeadlessCanvas(self.display, self._egl_surface)
+
+            self.context.attach(self.canvas)
+
+            self.dispatch_event('on_resize', self._width, self._height)
+
+
+__all__ = ["HeadlessWindow"]
diff --git a/pyglet/window/key.py b/pyglet/window/key.py
index e51b0a0..9b6ca0d 100644
--- a/pyglet/window/key.py
+++ b/pyglet/window/key.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/window/mouse.py b/pyglet/window/mouse.py
index f6146e3..5cd7a39 100644
--- a/pyglet/window/mouse.py
+++ b/pyglet/window/mouse.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/pyglet/window/win32/__init__.py b/pyglet/window/win32/__init__.py
index b1f5c95..037c3b4 100644
--- a/pyglet/window/win32/__init__.py
+++ b/pyglet/window/win32/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -106,6 +106,8 @@ class Win32Window(BaseWindow):
     _exclusive_mouse_lpos = None
     _exclusive_mouse_buttons = 0
     _mouse_platform_visible = True
+    _pending_click = False
+    _in_title_bar = False
     
     _keyboard_state = {0x02A: False, 0x036: False}  # For shift keys.
 
@@ -152,6 +154,10 @@ class Win32Window(BaseWindow):
                 self.WINDOW_STYLE_TOOL: (WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU,
                                          WS_EX_TOOLWINDOW),
                 self.WINDOW_STYLE_BORDERLESS: (WS_POPUP, 0),
+                self.WINDOW_STYLE_TRANSPARENT: (WS_OVERLAPPEDWINDOW,
+                                                WS_EX_LAYERED),
+                self.WINDOW_STYLE_OVERLAY: (WS_POPUP,
+                                            WS_EX_LAYERED | WS_EX_TRANSPARENT)
             }
             self._ws_style, self._ex_ws_style = styles[self._style]
 
@@ -213,6 +219,7 @@ class Win32Window(BaseWindow):
                 self._window_class.hInstance,
                 0)
 
+            # View Hwnd is for the client area so certain events (mouse events) don't trigger outside of area.
             self._view_hwnd = _user32.CreateWindowExW(
                 0,
                 self._view_window_class.lpszClassName,
@@ -224,6 +231,10 @@ class Win32Window(BaseWindow):
                 self._view_window_class.hInstance,
                 0)
 
+            if not self._view_hwnd:
+                last_error = _kernel32.GetLastError()
+                raise Exception("Failed to create handle", self, last_error, self._view_hwnd, self._hwnd)
+
             self._dc = _user32.GetDC(self._view_hwnd)
 
             # Only allow files being dropped if specified.
@@ -235,12 +246,13 @@ class Win32Window(BaseWindow):
                     _user32.ChangeWindowMessageFilterEx(self._hwnd, WM_COPYGLOBALDATA, MSGFLT_ALLOW, None)
 
                 _shell32.DragAcceptFiles(self._hwnd, True)
-                
-            # Register raw input keyboard to allow the window to receive input events.
-            raw_keyboard = RAWINPUTDEVICE(0x01, 0x06, 0, self._view_hwnd)
+
+            # Set the raw keyboard to handle shift state. This is required as legacy events cannot handle shift states
+            # when both keys are used together. View Hwnd as none changes focus to follow keyboard.
+            raw_keyboard = RAWINPUTDEVICE(0x01, 0x06, 0, None)
             if not _user32.RegisterRawInputDevices(
-                byref(raw_keyboard), 1, sizeof(RAWINPUTDEVICE)):
-                    print("Warning: Failed to register raw input keyboard. on_key events for shift keys will not be called.")
+                    byref(raw_keyboard), 1, sizeof(RAWINPUTDEVICE)):
+                print("Warning: Failed to unregister raw input keyboard.")
         else:
             # Window already exists, update it with new style
 
@@ -268,6 +280,11 @@ class Win32Window(BaseWindow):
             x, y = self._client_to_window_pos(*factory.get_location())
             _user32.SetWindowPos(self._hwnd, hwnd_after,
                                  x, y, width, height, SWP_FRAMECHANGED)
+        elif self.style == 'transparent' or self.style == "overlay":
+            _user32.SetLayeredWindowAttributes(self._hwnd, 0, 254, LWA_ALPHA)
+            if self.style == "overlay":
+                _user32.SetWindowPos(self._hwnd, HWND_TOPMOST, 0,
+                                     0, width, height, SWP_NOMOVE | SWP_NOSIZE)
         else:
             _user32.SetWindowPos(self._hwnd, hwnd_after,
                                  0, 0, width, height, SWP_NOMOVE | SWP_FRAMECHANGED)
@@ -347,6 +364,16 @@ class Win32Window(BaseWindow):
     def switch_to(self):
         self.context.set_current()
 
+    def update_transparency(self):
+        region = _gdi32.CreateRectRgn(0, 0, -1, -1)
+        bb = DWM_BLURBEHIND()
+        bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION
+        bb.hRgnBlur = region
+        bb.fEnable = True
+
+        _dwmapi.DwmEnableBlurBehindWindow(self._hwnd, ctypes.byref(bb))
+        _gdi32.DeleteObject(region)
+
     def flip(self):
         self.draw_mouse_cursor()
 
@@ -355,6 +382,9 @@ class Win32Window(BaseWindow):
                 if self._interval:
                     _dwmapi.DwmFlush()
 
+        if self.style in ('overlay', 'transparent'):
+            self.update_transparency()
+
         self.context.flip()
 
     def set_location(self, x, y):
@@ -443,6 +473,11 @@ class Win32Window(BaseWindow):
         if platform_visible == self._mouse_platform_visible:
             return
 
+        self._set_cursor_visibility(platform_visible)
+
+        self._mouse_platform_visible = platform_visible
+
+    def _set_cursor_visibility(self, platform_visible):
         # Avoid calling ShowCursor with the current visibility (which would
         # push the counter too far away from zero).
         global _win32_cursor_visible
@@ -450,21 +485,24 @@ class Win32Window(BaseWindow):
             _user32.ShowCursor(platform_visible)
             _win32_cursor_visible = platform_visible
 
-        self._mouse_platform_visible = platform_visible
+    def _update_clipped_cursor(self):
+        # Clip to client area, to prevent large mouse movements taking
+        # it outside the client area.
+        if self._in_title_bar or self._pending_click:
+            return
 
-    def _reset_exclusive_mouse_screen(self):
-        """Recalculate screen coords of mouse warp point for exclusive
-        mouse."""
-        p = POINT()
         rect = RECT()
         _user32.GetClientRect(self._view_hwnd, byref(rect))
-        _user32.MapWindowPoints(self._view_hwnd, HWND_DESKTOP, byref(rect), 2)
-        p.x = (rect.left + rect.right) // 2
-        p.y = (rect.top + rect.bottom) // 2
+        _user32.MapWindowPoints(self._view_hwnd, HWND_DESKTOP,
+                                byref(rect), 2)
 
-        # This is the point the mouse will be kept at while in exclusive
-        # mode.
-        self._exclusive_mouse_screen = p.x, p.y
+        # For some reason borders can be off 1 pixel, allowing cursor into frame/minimize/exit buttons?
+        rect.top += 1
+        rect.left += 1
+        rect.right -= 1
+        rect.bottom -= 1
+
+        _user32.ClipCursor(byref(rect))
 
     def set_exclusive_mouse(self, exclusive=True):
         if self._exclusive_mouse == exclusive and \
@@ -473,10 +511,7 @@ class Win32Window(BaseWindow):
 
         # Mouse: UsagePage = 1, Usage = 2
         raw_mouse = RAWINPUTDEVICE(0x01, 0x02, 0, None)
-        if exclusive:
-            raw_mouse.dwFlags = RIDEV_NOLEGACY
-            raw_mouse.hwndTarget = self._view_hwnd
-        else:
+        if not exclusive:
             raw_mouse.dwFlags = RIDEV_REMOVE
             raw_mouse.hwndTarget = None
 
@@ -487,15 +522,7 @@ class Win32Window(BaseWindow):
 
         self._exclusive_mouse_buttons = 0
         if exclusive and self._has_focus:
-            # Clip to client area, to prevent large mouse movements taking
-            # it outside the client area.
-            rect = RECT()
-            _user32.GetClientRect(self._view_hwnd, byref(rect))
-            _user32.MapWindowPoints(self._view_hwnd, HWND_DESKTOP,
-                                    byref(rect), 2)
-            _user32.ClipCursor(byref(rect))
-            # Release mouse capture in case is was acquired during mouse click
-            _user32.ReleaseCapture()
+            self._update_clipped_cursor()
         else:
             # Release clip
             _user32.ClipCursor(None)
@@ -732,7 +759,7 @@ class Win32Window(BaseWindow):
 
     def _get_modifiers(self, key_lParam=0):
         modifiers = 0
-        if _user32.GetKeyState(VK_SHIFT) & 0xff00:
+        if self._keyboard_state[0x036] or self._keyboard_state[0x02A]:
             modifiers |= key.MOD_SHIFT
         if _user32.GetKeyState(VK_CONTROL) & 0xff00:
             modifiers |= key.MOD_CTRL
@@ -744,6 +771,7 @@ class Win32Window(BaseWindow):
             modifiers |= key.MOD_NUMLOCK
         if _user32.GetKeyState(VK_SCROLL) & 0x00ff:  # toggle
             modifiers |= key.MOD_SCROLLLOCK
+
         if key_lParam:
             if key_lParam & (1 << 29):
                 modifiers |= key.MOD_ALT
@@ -781,7 +809,7 @@ class Win32Window(BaseWindow):
             symbol = key.RCTRL
         elif symbol == key.LALT and lParam & (1 << 24):
             symbol = key.RALT
-                    
+
         if wParam == VK_SHIFT:
             return  # Let raw input handle this instead.
 
@@ -804,6 +832,23 @@ class Win32Window(BaseWindow):
         else:
             return None
 
+    @Win32EventHandler(WM_NCLBUTTONDOWN)
+    def _event_ncl_button_down(self, msg, wParam, lParam):
+        self._in_title_bar = True
+
+    @Win32EventHandler(WM_CAPTURECHANGED)
+    def _event_capture_changed(self, msg, wParam, lParam):
+        self._in_title_bar = False
+
+        if self._exclusive_mouse:
+            state = _user32.GetAsyncKeyState(VK_LBUTTON)
+            if not state & 0x8000:  # released
+                if self._pending_click:
+                    self._pending_click = False
+
+                if self._has_focus or not self._hidden:
+                    self._update_clipped_cursor()
+
     @Win32EventHandler(WM_CHAR)
     def _event_char(self, msg, wParam, lParam):
         text = chr(wParam)
@@ -811,7 +856,6 @@ class Win32Window(BaseWindow):
             self.dispatch_event('on_text', text)
         return 0
 
-    @ViewEventHandler
     @Win32EventHandler(WM_INPUT)
     def _event_raw_input(self, msg, wParam, lParam):
         hRawInput = cast(lParam, HRAWINPUT)
@@ -823,37 +867,8 @@ class Win32Window(BaseWindow):
         if inp.header.dwType == RIM_TYPEMOUSE:
             if not self._exclusive_mouse:
                 return 0
-                
-            rmouse = inp.data.mouse
 
-            if rmouse.usButtonFlags & RI_MOUSE_LEFT_BUTTON_DOWN:
-                self.dispatch_event('on_mouse_press', 0, 0, mouse.LEFT,
-                                    self._get_modifiers())
-                self._exclusive_mouse_buttons |= mouse.LEFT
-            if rmouse.usButtonFlags & RI_MOUSE_LEFT_BUTTON_UP:
-                self.dispatch_event('on_mouse_release', 0, 0, mouse.LEFT,
-                                    self._get_modifiers())
-                self._exclusive_mouse_buttons &= ~mouse.LEFT
-            if rmouse.usButtonFlags & RI_MOUSE_RIGHT_BUTTON_DOWN:
-                self.dispatch_event('on_mouse_press', 0, 0, mouse.RIGHT,
-                                    self._get_modifiers())
-                self._exclusive_mouse_buttons |= mouse.RIGHT
-            if rmouse.usButtonFlags & RI_MOUSE_RIGHT_BUTTON_UP:
-                self.dispatch_event('on_mouse_release', 0, 0, mouse.RIGHT,
-                                    self._get_modifiers())
-                self._exclusive_mouse_buttons &= ~mouse.RIGHT
-            if rmouse.usButtonFlags & RI_MOUSE_MIDDLE_BUTTON_DOWN:
-                self.dispatch_event('on_mouse_press', 0, 0, mouse.MIDDLE,
-                                    self._get_modifiers())
-                self._exclusive_mouse_buttons |= mouse.MIDDLE
-            if rmouse.usButtonFlags & RI_MOUSE_MIDDLE_BUTTON_UP:
-                self.dispatch_event('on_mouse_release', 0, 0, mouse.MIDDLE,
-                                    self._get_modifiers())
-                self._exclusive_mouse_buttons &= ~mouse.MIDDLE
-            if rmouse.usButtonFlags & RI_MOUSE_WHEEL:
-                delta = SHORT(rmouse.usButtonData).value
-                self.dispatch_event('on_mouse_scroll',
-                                    0, 0, 0, delta / float(WHEEL_DELTA))
+            rmouse = inp.data.mouse
 
             if rmouse.usFlags & 0x01 == MOUSE_MOVE_RELATIVE:
                 if rmouse.lLastX != 0 or rmouse.lLastY != 0:
@@ -885,29 +900,30 @@ class Win32Window(BaseWindow):
                         self.dispatch_event('on_mouse_motion', 0, 0,
                                             rel_x, rel_y)
                     self._exclusive_mouse_lpos = rmouse.lLastX, rmouse.lLastY
-                    
+
         elif inp.header.dwType == RIM_TYPEKEYBOARD:
             if inp.data.keyboard.VKey == 255:
                 return 0
 
             key_up = inp.data.keyboard.Flags & RI_KEY_BREAK
-  
+
             if inp.data.keyboard.MakeCode == 0x02A:  # LEFT_SHIFT
                 if not key_up and not self._keyboard_state[0x02A]:
-                    self.dispatch_event('on_key_press', key.LSHIFT, self._get_modifiers())
                     self._keyboard_state[0x02A] = True
+                    self.dispatch_event('on_key_press', key.LSHIFT, self._get_modifiers())
 
                 elif key_up and self._keyboard_state[0x02A]:
-                    self.dispatch_event('on_key_release', key.LSHIFT, self._get_modifiers())
                     self._keyboard_state[0x02A] = False
-            
-            elif inp.data.keyboard.MakeCode == 0x036: # RIGHT SHIFT
+                    self.dispatch_event('on_key_release', key.LSHIFT, self._get_modifiers())
+
+            elif inp.data.keyboard.MakeCode == 0x036:  # RIGHT SHIFT
                 if not key_up and not self._keyboard_state[0x036]:
-                    self.dispatch_event('on_key_press', key.RSHIFT, self._get_modifiers())
                     self._keyboard_state[0x036] = True
+                    self.dispatch_event('on_key_press', key.RSHIFT, self._get_modifiers())
+
                 elif key_up and self._keyboard_state[0x036]:
+                    self._keyboard_state[0x036] = False
                     self.dispatch_event('on_key_release', key.RSHIFT, self._get_modifiers())
-                    self._keyboard_state[0x036] = False            
 
         return 0
 
@@ -1076,6 +1092,10 @@ class Win32Window(BaseWindow):
         if not self._fullscreen:
             self._width, self._height = w, h
         self._update_view_location(self._width, self._height)
+
+        if self._exclusive_mouse:
+            self._update_clipped_cursor()
+
         self.switch_to()
         self.dispatch_event('on_resize', self._width, self._height)
         return 0
@@ -1100,45 +1120,63 @@ class Win32Window(BaseWindow):
         self.dispatch_event('on_move', x, y)
         return 0
 
-    @Win32EventHandler(WM_EXITSIZEMOVE)
+    @Win32EventHandler(WM_SETCURSOR)
+    def _event_setcursor(self, msg, wParam, lParam):
+        if self._exclusive_mouse and not self._mouse_platform_visible:
+            lo, hi = self._get_location(lParam)
+            if lo == HTCLIENT:  # In frame
+                self._set_cursor_visibility(False)
+                return 1
+            elif lo in (HTCAPTION, HTCLOSE, HTMAXBUTTON, HTMINBUTTON):  # Allow in
+                self._set_cursor_visibility(True)
+                return 1
+
+    @Win32EventHandler(WM_ENTERSIZEMOVE)
     def _event_entersizemove(self, msg, wParam, lParam):
+        self._moving = True
         from pyglet import app
         if app.event_loop is not None:
             app.event_loop.exit_blocking()
 
-    """
-    # Alternative to using WM_SETFOCUS and WM_KILLFOCUS.  Which
-    # is better?
+    @Win32EventHandler(WM_EXITSIZEMOVE)
+    def _event_exitsizemove(self, msg, wParam, lParam):
+        self._moving = False
+        from pyglet import app
+        if app.event_loop is not None:
+            app.event_loop.exit_blocking()
 
-    @Win32EventHandler(WM_ACTIVATE)
-    def _event_activate(self, msg, wParam, lParam):
-        if wParam & 0xffff == WA_INACTIVE:
-            self.dispatch_event('on_deactivate')
-        else:
-            self.dispatch_event('on_activate')
-            _user32.SetFocus(self._hwnd)
-        return 0
-    """
+        if self._exclusive_mouse:
+            self._update_clipped_cursor()
 
     @Win32EventHandler(WM_SETFOCUS)
     def _event_setfocus(self, msg, wParam, lParam):
         self.dispatch_event('on_activate')
         self._has_focus = True
 
+        if self._exclusive_mouse:
+            if _user32.GetAsyncKeyState(VK_LBUTTON):
+                self._pending_click = True
+
         self.set_exclusive_keyboard(self._exclusive_keyboard)
         self.set_exclusive_mouse(self._exclusive_mouse)
+
         return 0
 
     @Win32EventHandler(WM_KILLFOCUS)
     def _event_killfocus(self, msg, wParam, lParam):
         self.dispatch_event('on_deactivate')
         self._has_focus = False
+
         exclusive_keyboard = self._exclusive_keyboard
         exclusive_mouse = self._exclusive_mouse
         # Disable both exclusive keyboard and mouse
         self.set_exclusive_keyboard(False)
         self.set_exclusive_mouse(False)
 
+        # Reset shift state on Window focus loss.
+        for symbol in self._keyboard_state:
+            self._keyboard_state[symbol] = False
+
         # But save desired state and note that we lost focus
         # This will allow to reset the correct mode once we regain focus
         self._exclusive_keyboard = exclusive_keyboard
@@ -1150,12 +1188,14 @@ class Win32Window(BaseWindow):
     @Win32EventHandler(WM_GETMINMAXINFO)
     def _event_getminmaxinfo(self, msg, wParam, lParam):
         info = MINMAXINFO.from_address(lParam)
+
         if self._minimum_size:
             info.ptMinTrackSize.x, info.ptMinTrackSize.y = \
                 self._client_to_window_size(*self._minimum_size)
         if self._maximum_size:
             info.ptMaxTrackSize.x, info.ptMaxTrackSize.y = \
                 self._client_to_window_size(*self._maximum_size)
+
         return 0
 
     @Win32EventHandler(WM_ERASEBKGND)
@@ -1198,3 +1238,6 @@ class Win32Window(BaseWindow):
         # Reverse Y and call event.
         self.dispatch_event('on_file_drop', point.x, self._height - point.y, paths)
         return 0
+
+
+__all__ = ["Win32EventHandler", "Win32Window"]
diff --git a/pyglet/window/xlib/__init__.py b/pyglet/window/xlib/__init__.py
index b0f530d..1c21144 100644
--- a/pyglet/window/xlib/__init__.py
+++ b/pyglet/window/xlib/__init__.py
@@ -1,7 +1,7 @@
 # ----------------------------------------------------------------------------
 # pyglet
 # Copyright (c) 2006-2008 Alex Holkner
-# Copyright (c) 2008-2020 pyglet contributors
+# Copyright (c) 2008-2022 pyglet contributors
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -123,8 +123,6 @@ XlibEventHandler = _PlatformEventHandler
 ViewEventHandler = _ViewEventHandler
 
 
-
-
 class XlibWindow(BaseWindow):
     _x_display = None               # X display connection
     _x_screen_id = None             # X screen index
@@ -232,6 +230,8 @@ class XlibWindow(BaseWindow):
             root = xlib.XRootWindow(self._x_display, self._x_screen_id)
 
             visual_info = self.config.get_visual_info()
+            if self.style in ('transparent', 'overlay'):
+                xlib.XMatchVisualInfo(self._x_display, self._x_screen_id, 32, xlib.TrueColor, visual_info)
 
             visual = visual_info.visual
             visual_id = xlib.XVisualIDFromVisual(visual)
@@ -252,6 +252,11 @@ class XlibWindow(BaseWindow):
             #            unconditionally.
             mask = xlib.CWColormap | xlib.CWBitGravity | xlib.CWBackPixel
 
+            if self.style in ('transparent', 'overlay'):
+                mask |= xlib.CWBorderPixel
+                window_attributes.border_pixel = 0
+                window_attributes.background_pixel = 0
+
             if self._fullscreen:
                 width, height = self.screen.width, self.screen.height
                 self._view_x = (width - self._width) // 2
@@ -367,7 +372,7 @@ class XlibWindow(BaseWindow):
         }
         if self._style in styles:
             self._set_atoms_property('_NET_WM_WINDOW_TYPE', (styles[self._style],))
-        elif self._style == self.WINDOW_STYLE_BORDERLESS:
+        elif self._style in (self.WINDOW_STYLE_BORDERLESS, self.WINDOW_STYLE_OVERLAY):
             MWM_HINTS_DECORATIONS = 1 << 1
             PROP_MWM_HINTS_ELEMENTS = 5
             mwmhints = mwmhints_t()
@@ -1114,8 +1119,7 @@ class XlibWindow(BaseWindow):
                     motion = self._event_text_motion(symbol, modifiers)
                     if motion:
                         if modifiers & key.MOD_SHIFT:
-                            self.dispatch_event(
-                                'on_text_motion_select', motion)
+                            self.dispatch_event('on_text_motion_select', motion)
                         else:
                             self.dispatch_event('on_text_motion', motion)
                     elif text and not modifiers_ctrl:
@@ -1165,7 +1169,7 @@ class XlibWindow(BaseWindow):
     @XlibEventHandler(xlib.MotionNotify)
     def _event_motionnotify_view(self, ev):
         x = ev.xmotion.x
-        y = self.height - ev.xmotion.y
+        y = self.height - ev.xmotion.y - 1
 
         if self._mouse_in_window:
             dx = x - self._mouse_x
@@ -1173,8 +1177,7 @@ class XlibWindow(BaseWindow):
         else:
             dx = dy = 0
 
-        if self._applied_mouse_exclusive \
-                and (ev.xmotion.x, ev.xmotion.y) == self._mouse_exclusive_client:
+        if self._applied_mouse_exclusive and (ev.xmotion.x, ev.xmotion.y) == self._mouse_exclusive_client:
             # Ignore events caused by XWarpPointer
             self._mouse_x = x
             self._mouse_y = y
@@ -1225,7 +1228,7 @@ class XlibWindow(BaseWindow):
         if buttons:
             # Drag event
             x = ev.xmotion.x - self._view_x
-            y = self._height - (ev.xmotion.y - self._view_y)
+            y = self._height - (ev.xmotion.y - self._view_y - 1)
 
             if self._mouse_in_window:
                 dx = x - self._mouse_x
@@ -1464,6 +1467,10 @@ class XlibWindow(BaseWindow):
                 self.dispatch_event('on_mouse_scroll', x, y, 0, 1)
             elif ev.xbutton.button == 5:
                 self.dispatch_event('on_mouse_scroll', x, y, 0, -1)
+            elif ev.xbutton.button == 6:
+                self.dispatch_event('on_mouse_scroll', x, y, -1, 0)
+            elif ev.xbutton.button == 7:
+                self.dispatch_event('on_mouse_scroll', x, y, 1, 0)
             elif ev.xbutton.button < len(self._mouse_buttons):
                 self._mouse_buttons[ev.xbutton.button] = True
                 self.dispatch_event('on_mouse_press', x, y, button, modifiers)
@@ -1556,3 +1563,6 @@ class XlibWindow(BaseWindow):
     def _event_unmapnotify(self, ev):
         self._mapped = False
         self.dispatch_event('on_hide')
+
+
+__all__ = ["XlibEventHandler", "XlibWindow"]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..3bb58e5
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,20 @@
+[build-system]
+requires = ["flit_core >=2,<4", "wheel"]
+build-backend = ["flit_core.buildapi", "flit_core.wheel"]
+
+[tool.flit.metadata]
+module = "pyglet"
+author = "Alex Holkner"
+author-email = "alex.holkner@gmail.com"
+home-page = "http://pyglet.org/"
+description-file = "README.md"
+requires-python=">=3.6"
+license='BSD'
+
+[tool.flit.metadata.requires-extra]
+test = ["pytest"]
+doc = ["sphinx"]
+
+[tool.flit.sdist]
+include = ["pyglet"]
+#exclude = ["doc/*.html"]
\ No newline at end of file
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..17c39b8
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,10 @@
+[pytest]
+addopts = -s --strict-markers
+norecursedirs = contrib doc examples experimental tools website
+markers =
+  unit: Unit tests are fast and only test a single module.
+  integration: Integration tests integrate several modules or integrate with the OS.
+  interactive: Interactive tests require interaction with the user to complete.
+  requires_user_action: Test cannot run without user interaction.
+  requires_user_validation: Test cannot validate without user interaction, but can run for sanity.
+  only_interactive: This test can only be run in interactive mode.
diff --git a/setup.py b/setup.py
index 7e1022a..e19752d 100644
--- a/setup.py
+++ b/setup.py
@@ -38,10 +38,10 @@ setup_info = dict(
         'Operating System :: Microsoft :: Windows',
         'Operating System :: POSIX :: Linux',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
+        'Programming Language :: Python :: 3.9',
         'Topic :: Games/Entertainment',
         'Topic :: Software Development :: Libraries :: Python Modules',
     ],
diff --git a/tests/interactive/screenshots/session/tests.interactive.graphics.test_multitexture.test_multitexture.001.png b/tests/interactive/screenshots/session/tests.interactive.graphics.test_multitexture.test_multitexture.001.png
new file mode 100644
index 0000000..8ef45ce
Binary files /dev/null and b/tests/interactive/screenshots/session/tests.interactive.graphics.test_multitexture.test_multitexture.001.png differ
diff --git a/tests/unit/media/test_synthesis.py b/tests/unit/media/test_synthesis.py
index d0ea0cb..f626bfd 100644
--- a/tests/unit/media/test_synthesis.py
+++ b/tests/unit/media/test_synthesis.py
@@ -1,7 +1,7 @@
-import unittest
-
 from ctypes import sizeof
 from io import BytesIO
+import unittest
+import wave
 
 from pyglet.media.synthesis import *
 
@@ -79,9 +79,8 @@ class SynthesisSourceTest:
         source_name = self.source_class.__name__.lower()
         filename = "synthesis_{0}_{1}_{2}_1ch.wav".format(source_name, sample_size, sample_rate)
 
-        with open(get_test_data_file('media', filename), 'rb') as f:
-            # discard the wave header:
-            loaded_bytes = f.read()[44:]
+        with wave.open(get_test_data_file('media', filename)) as f:
+            loaded_bytes = f.readframes(-1)
             source.seek(0)
             generated_data = source.get_audio_data(source._max_offset)
             bytes_buffer = BytesIO(generated_data.data).getvalue()
diff --git a/tests/unit/test_events.py b/tests/unit/test_events.py
index 766136a..d5f1349 100644
--- a/tests/unit/test_events.py
+++ b/tests/unit/test_events.py
@@ -3,9 +3,10 @@
 
 import pytest
 import types
-import sys
-import pyglet
 from tests import mock
+
+import pyglet
+
 from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED
 
 
@@ -183,7 +184,6 @@ class DummyHandler:
         return True
 
 
-@pytest.mark.skipif(sys.version_info < (3, 4), reason="requires python3.4")
 def test_weakref_to_instance_method(dispatcher):
     import weakref
     dispatcher.register_event_type('mock_event')
@@ -195,7 +195,6 @@ def test_weakref_to_instance_method(dispatcher):
     assert watcher.called
 
 
-@pytest.mark.skipif(sys.version_info < (3, 4), reason="requires python3.4")
 def test_weakref_to_instance(dispatcher):
     import weakref
     dispatcher.register_event_type('mock_event')
@@ -207,7 +206,6 @@ def test_weakref_to_instance(dispatcher):
     assert watcher.called
 
 
-@pytest.mark.skipif(sys.version_info < (3, 4), reason="requires python3.4")
 def test_weakref_deleted_when_instance_is_deleted(dispatcher):
     dispatcher.register_event_type('mock_event')
     handler = DummyHandler()
diff --git a/tests/unit/test_font.py b/tests/unit/test_font.py
index b3c30ee..dcc987b 100644
--- a/tests/unit/test_font.py
+++ b/tests/unit/test_font.py
@@ -11,7 +11,7 @@ def test_load_privatefont(test_data):
 
     pyglet.font.add_file(test_data.get_file('fonts', 'action_man.ttf'))
     action_man_font = pyglet.font.load("Action Man", size=12, dpi=96)
-    assert action_man_font.logfont.lfFaceName.decode("utf-8") == "Action Man"
+    assert action_man_font.logfont.lfFaceName == "Action Man"
 
 
 @require_platform(Platform.WINDOWS)
@@ -20,4 +20,4 @@ def test_load_privatefont_from_list(test_data):
 
     pyglet.font.add_file(test_data.get_file('fonts', 'action_man.ttf'))
     action_man_font = pyglet.font.load(["Action Man"], size=12, dpi=96)
-    assert action_man_font.logfont.lfFaceName.decode("utf-8") == "Action Man"
+    assert action_man_font.logfont.lfFaceName == "Action Man"
diff --git a/tools/al_info.py b/tools/al_info.py
new file mode 100644
index 0000000..f6b61c2
--- /dev/null
+++ b/tools/al_info.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+
+"""Print OpenAL driver information.
+
+Options:
+  -d <device>   Optionally specify device to query.
+"""
+
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+import ctypes
+import optparse
+import sys
+
+from pyglet.util import asbytes
+from pyglet.media.drivers import openal
+from pyglet.media.drivers.openal.interface import al
+from pyglet.media.drivers.openal.interface import alc
+
+
+def split_nul_strings(s):
+    # NUL-separated list of strings, double-NUL-terminated.
+    nul = False
+    i = 0
+    while True:
+        if s[i] == b'\x00':
+            if nul:
+                break
+            else:
+                nul = True
+        else:
+            nul = False
+        i += 1
+    s = s[:i - 1]
+    return s.split(b'\x00')
+
+
+if __name__ == '__main__':
+    op = optparse.OptionParser()
+    op.add_option('-d', '--device', dest='device',
+                  help='use device DEVICE', metavar='DEVICE')
+    (options, args) = op.parse_args(sys.argv[1:])
+
+    default_device = ctypes.cast(
+        alc.alcGetString(None, alc.ALC_DEFAULT_DEVICE_SPECIFIER), ctypes.c_char_p).value
+    capture_default_device = ctypes.cast(
+        alc.alcGetString(None, alc.ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER), ctypes.c_char_p).value
+
+    print('Default device:         %s' % default_device)
+    print('Default capture device: %s' % capture_default_device)
+
+    if alc.alcIsExtensionPresent(None, ctypes.create_string_buffer(b'ALC_ENUMERATION_EXT')):
+        devices = split_nul_strings(alc.alcGetString(None, alc.ALC_DEVICE_SPECIFIER))
+        capture_devices = split_nul_strings(alc.alcGetString(None, alc.ALC_CAPTURE_DEVICE_SPECIFIER))
+
+        print('Devices:                %s' % b', '.join(devices))
+        print('Capture devices:        %s\n' % b', '.join(capture_devices))
+
+    if options.device:
+        print('Using device "%s"...' % options.device)
+        driver = openal.create_audio_driver(asbytes(options.device))
+    else:
+        print('Using default device...')
+        driver = openal.create_audio_driver()
+
+    print('OpenAL version %d.%d' % driver.get_version())
+    print('Extensions:              %s' % ', '.join(driver.get_extensions()))
diff --git a/tools/ffmpeg/bokeh_timeline.py b/tools/ffmpeg/bokeh_timeline.py
new file mode 100644
index 0000000..7c382be
--- /dev/null
+++ b/tools/ffmpeg/bokeh_timeline.py
@@ -0,0 +1,146 @@
+"""
+Usage
+
+    bokeh_timeline.py sample
+
+Renders media player's internal state graphically using bokeh
+
+Arguments
+    sample: sample to report
+
+The output will be written to session's output dir under
+    reports/sample.timeline.html
+
+Notice the plot can be zoomed live with the mouse wheel, but you must click
+the button that looks as a distorted 'OP'; it also does pan with mouse drag.
+
+Example
+
+    bokeh_timeline.py small.mp4
+
+will write the output to report/small.mp4.timeline.html
+"""
+# just in case, this version was developed and tested with bokeh vs 0.12.6 (win)
+
+import os
+import sys
+
+from bokeh.layouts import column
+from bokeh.models import LinearAxis, Range1d
+from bokeh.plotting import figure, output_file, show
+
+import fs
+import mpexceptions
+import timeline
+
+
+def main(sample):
+    try:
+        pathserv = fs.get_path_info_for_active_session()
+    except mpexceptions.ExceptionUndefinedSamplesDir:
+        print("The env var 'pyglet_mp_samples_dir' is not defined.")
+        return 1
+    except mpexceptions.ExceptionNoSessionIsActive:
+        print("*** Error, no session active.")
+        return 1
+
+    bokeh_render_timeline(pathserv, sample)
+
+
+def bokeh_render_timeline(pathserv, sample):
+    infile = pathserv.report_filename(sample, "timeline.pkl", False)
+    if not os.path.isfile(infile):
+        timeline.save_timeline(pathserv, sample, ".pkl")
+
+    timeline_postprocessed = fs.pickle_load(infile)
+    info = unpack_timeline(timeline_postprocessed)
+    outfile = pathserv.report_filename(sample, "timeline.html", False)
+    make_plot(info, outfile)
+
+
+def unpack_timeline(timeline_postprocessed):
+    timeline, current_time_nones, audio_time_nones = timeline_postprocessed
+    (wall_times, pyglet_times, audio_times, current_times,
+     frame_nums, rescheds) = zip(*timeline)
+
+    if current_time_nones:
+        x_vnones, y_vnones = zip(*current_time_nones)
+    else:
+        x_vnones, y_vnones = [], []
+
+    if audio_time_nones:
+        x_anones, y_anones = zip(*audio_time_nones)
+    else:
+        x_anones, y_anones = [], []
+
+    return (wall_times, pyglet_times, audio_times,
+            current_times, frame_nums, rescheds,
+            x_vnones, y_vnones,
+            x_anones, y_anones)
+
+
+def make_plot(info, outfile):
+    # prepare some data
+    (wall_times, pyglet_times, audio_times,
+     current_times, frame_nums, rescheds,
+     x_vnones, y_vnones,
+     x_anones, y_anones) = info
+
+    # output to static HTML file
+    output_file(outfile)
+
+    # main plot
+    p = figure(
+       tools="pan,wheel_zoom,reset,save",
+       y_axis_type="linear", y_range=[0.000, wall_times[-1]], title="timeline",
+       x_axis_label='wall_time', y_axis_label='time',
+       plot_width=600, plot_height=600
+    )
+
+    # add some renderers
+    p.line(wall_times, wall_times, legend="wall_time")
+    #p.line(wall_times, pyglet_times, legend="pyglet_time", line_width=3)
+    p.line(wall_times, current_times, legend="current_times", line_color="red")
+    p.line(wall_times, audio_times, legend="audio_times", line_color="orange", line_dash="4 4")
+
+    p.circle(x_vnones, y_vnones, legend="current time nones", fill_color="green", size=8)
+    p.circle(x_anones, y_anones, legend="audio time nones", fill_color="red", size=6)
+
+    # secondary y-axis for frame_num
+    p.extra_y_ranges = {"frame_num": Range1d(start=0, end=frame_nums[-1])}
+    p.line(wall_times, frame_nums, legend="frame_num",
+           line_color="black", y_range_name="frame_num")
+    p.add_layout(LinearAxis(y_range_name="frame_num", axis_label="frame num"), 'left')
+
+    p.legend.location = "bottom_right"
+    # show the results
+    #show(p)
+
+    # secondary plot for rescheduling times
+    q = figure(
+       tools="pan,wheel_zoom,reset,save",
+       y_axis_type="linear", y_range=[-0.3, 0.3], title="rescheduling time",
+       x_axis_label='wall_time', y_axis_label='rescheduling time',
+       plot_width=600, plot_height=150
+    )
+    q.line(wall_times, rescheds)
+
+    show(column(p, q))
+
+
+def usage():
+    print(__doc__)
+    sys.exit(1)
+
+
+def sysargs_to_mainargs():
+    """builds main args from sys.argv"""
+    if len(sys.argv) < 2:
+        usage()
+    sample = sys.argv[1]
+    return sample
+
+
+if __name__ == '__main__':
+    sample = sysargs_to_mainargs()
+    main(sample)
diff --git a/tools/ffmpeg/compare.py b/tools/ffmpeg/compare.py
new file mode 100644
index 0000000..908dbc6
--- /dev/null
+++ b/tools/ffmpeg/compare.py
@@ -0,0 +1,85 @@
+"""
+Usage
+
+    compare.py --reldir=relpath other_session
+
+Builds a reports comparing the active session with other_session.
+
+Outputs to samples_dir/relpath/comparison_<session>_<other_session>.txt
+"""
+import os
+import sys
+
+import extractors
+import fs
+from pyglet.media import instrumentation as ins
+import mpexceptions
+import reports
+
+
+def main(relative_out_dir, other_session):
+    try:
+        pathserv = fs.get_path_info_for_active_session()
+    except mpexceptions.ExceptionUndefinedSamplesDir:
+        print("The env var 'pyglet_mp_samples_dir' is not defined.")
+        return 1
+    except mpexceptions.ExceptionNoSessionIsActive:
+        print("*** Error, no session active.")
+        return 1
+
+    try:
+        pathserv_other = fs.get_path_info_for_session(other_session)
+    except mpexceptions.ExceptionNoSessionWithThatName:
+        print("No session by that name")
+        return 1
+
+    outdir = os.path.join(fs.get_current_samples_dir(), relative_out_dir)
+    if not os.path.isdir(outdir):
+        os.mkdir(outdir)
+
+    compare_sessions_to_file(pathserv, pathserv_other, outdir)
+    return 0
+
+
+def compare_sessions_to_file(pathserv, pathserv_other, outdir):
+    """compares two sessions, outputs text to file
+    outdir/comparison_<session>_<other_session>.txt
+    """
+    outfile = os.path.join(outdir, "comparison_%s_%s.txt" %
+                           (pathserv.session, pathserv_other.session))
+    text = compare_sessions_to_text(pathserv, pathserv_other)
+    fs.txt_save(text, outfile)
+
+
+def compare_sessions_to_text(pathserv, pathserv_other):
+    """compares two sessions, returns text"""
+    count_bads = ins.CountBads()
+    comparison = extractors.CollectSessionComparisonData(pathserv,
+                                                         pathserv_other,
+                                                         count_bads.count_bads)
+    text = reports.report_session_comparison(comparison)
+    return text
+
+
+def sysargs_to_mainargs():
+    """builds main args from sys.argv"""
+    relative_out_dir = None
+    if len(sys.argv) > 1 and sys.argv[1].startswith("--"):
+        a = sys.argv.pop(1)
+        if a.startswith("--help"):
+            print(__doc__)
+            sys.exit(1)
+        elif a.startswith("--reldir="):
+            relative_out_dir = a[len("--reldir="):]
+        else:
+            print("*** Error, Unknown option:", a)
+            print(__doc__)
+            sys.exit(1)
+
+    other_session = sys.argv[1]
+    return relative_out_dir, other_session
+
+
+if __name__ == "__main__":
+    relative_out_dir, other_session = sysargs_to_mainargs()
+    main(relative_out_dir, other_session)
diff --git a/tools/ffmpeg/configure.py b/tools/ffmpeg/configure.py
new file mode 100644
index 0000000..09b05aa
--- /dev/null
+++ b/tools/ffmpeg/configure.py
@@ -0,0 +1,302 @@
+"""configure.py script to get / set some session configurable values; mp.py is
+an alias for this command.
+"""
+
+import sys
+
+import fs
+import mpexceptions
+
+
+template_cmd = (
+"""
+Usage
+
+    &cmd subcommand [args]
+
+Subcommands
+
+    new session [playlist] : Creates a new session, sets it as the active one
+    activate session : activates a session 
+    deactivate : no session will be active
+    protect [target]: forbids overwrite of session data
+    status : prints configuration for the active session
+    help [subcommand] : prints help for the given subcommand or topic
+    list : list all sessions associated the current samples_dir 
+
+Creates and manages pyglet media_player debug session configurations.
+
+Most commands and subcommands need an environment variable pyglet_mp_samples_dir
+to be set to the directory where the media samples reside.
+
+The configuration stores some values used when other commands are executed.
+
+    samples_dir: directory where the samples reside
+    session: a name to identify a particular session
+    session_dir: directory to store all the session's output files, set to  
+                 samples_dir/session
+    permissions to overwrite or not raw data collected or generated reports
+    playlist: subset of samples to play
+
+This command can be called both as 'configure.py' or 'mp.py', they do the same.
+
+"""
+)
+
+template_subcommands = (
+"""
+new
+Usage
+
+    &cmd new session [playlist_file]
+
+Arguments
+
+    session       : a name to identify this particular session
+    playlist_file : file with a list of samples
+
+Creates a new session and a directory to save session outputs.
+
+The configuration is saved to disk and the session is made the active one.
+
+If playlist_file os specified, only the samples in the playlist will be
+considered as targets for a general command. 
+
+General commands will use the configuration of the active session to know from
+where to read and write data.
+
+@
+activate
+Usage
+
+    &cmd activate session
+
+Arguments
+
+    session: name of session to activate.
+
+Sets the session as the active one.
+
+General commands will use the configuration of the active session to know from
+where to read and write data.
+
+@
+deactivate
+Usage
+
+    &cmd deactivate
+
+Makes the current session inactive.
+
+No session will be active, so general commands will not modify any session data.
+
+Mostly as a precaution so that a slip in shell history does not overwrite
+anything.
+@
+protect
+Usage
+
+    &cmd protect target
+
+Arguments
+
+target: one of
+
+"raw_data"
+    Forbids general commands to overwrite session's raw data captured.
+    Helps to not mix in the same session results obtained in different conditions.
+
+"reports"
+    Forbids general commands to overwrite session's generated reports.
+    Useful if the reports are manually annotated.
+
+@
+status
+Usage
+
+    &cmd status [session]
+
+Arguments
+
+    session: name of session
+
+Prints the configuration for the active session.
+
+@
+help
+Usage
+
+    &cmd help [subcommand or topic]
+
+Subcommands
+
+    new session [playlist] : Creates a new session, sets it as the active one
+    activate session : activates a session 
+    deactivate : no session will be active
+    protect [target]: forbids overwrite of session data
+    status : prints configuration for the active session
+    help [subcommand] : prints help for the given subcommand or topic
+    list : list all sessions associated the current samples_dir 
+
+Topics
+
+    layout  : data layout on disk
+    session : sessions what and whys
+    workflow: common workflows
+    gencmds : general commands
+@
+list
+Usage
+
+    &cmd list
+
+List all sessions associated with the current sample_dir
+
+
+"""
+)
+
+
+def sub_and_template_from_section(s):
+    subcommand, template = s[1:].split("\n", 1)
+    return subcommand, template
+
+
+def help_texts_from_template(cmd):
+    cmd_help = template_cmd.replace("&cmd", cmd)
+
+    all_subcommands = template_subcommands.replace("&cmd", cmd)
+    parts = all_subcommands.split("@")
+    pairs = [sub_and_template_from_section(s) for s in parts]
+    subcommands_help = {a: b for a, b in pairs}
+
+    return cmd_help, subcommands_help
+
+
+def test_help_strings():
+    cmd_help, subcommands_help = help_texts_from_template('mp')
+    print(cmd_help)
+    for e in subcommands_help:
+        print("sub:", repr(e))
+        print("|%s|" % subcommands_help[e])
+
+
+def show_configuration(keys_to_show=None):
+    pathserv = fs.get_path_info_for_active_session()
+    conf = fs.json_load(pathserv.configuration_filename())
+    if keys_to_show is None:
+        keys_to_show = conf.keys()
+    print("session:", pathserv.session)
+    for k in sorted(keys_to_show):
+        print("\t%s: %s" % (k, conf[k]))
+
+
+def sub_activate():
+    if len(sys.argv) < 3:
+        print("*** Error, missing argument.\n")
+        print(subcommands_help["activate"])
+        return 1
+    session = sys.argv[2]
+    fs.activation_set_to(session)
+    return 0
+
+
+def sub_deactivate():
+    session = None
+    fs.activation_set_to(session)
+    return 0
+
+
+def sub_help():
+    if len(sys.argv) < 3:
+        topic = "help"
+    else:
+        topic = sys.argv[2]
+    print(subcommands_help[topic])
+    return 0
+
+
+def sub_list():
+    samples_dir = fs.get_current_samples_dir()
+    sessions = fs.get_sessions(samples_dir)
+    for session in sorted(sessions):
+        print(session)
+    return 0
+
+
+def sub_new():
+    if len(sys.argv) < 3:
+        print("*** Error, missing argument.\n")
+        print(subcommands_help["new"])
+        return 1
+
+    session = sys.argv[2]
+    if len(sys.argv) > 3:
+        playlist_file = sys.argv[3]
+    else:
+        playlist_file = None
+    fs.new_session(session, playlist_file)
+    return 0
+
+
+def sub_protect():
+    if len(sys.argv) < 3:
+        print("*** Error, missing argument.\n")
+        print(subcommands_help["protect"])
+        return 1
+
+    name = sys.argv[2]
+    if name not in {"raw_data", "reports"}:
+        print("*** Error, unknown name to protect. name:", name)
+        print(subcommands_help["protect"])
+        return 1
+    modify = {("protect_" + name): True}
+    fs.update_active_configuration(modify)
+    return 0
+
+
+def sub_status():
+    show_configuration()
+    return 0
+
+
+def dispatch_subcommand(caller):
+    global cmd_help, subcommands_help
+    cmd_help, subcommands_help = help_texts_from_template(caller)
+    if len(sys.argv) < 2:
+        sub = "help"
+    else:
+        sub = sys.argv[1]
+    try:
+        returncode = globals()["sub_" + sub]()
+    except KeyError:
+        print("*** Error, unknown subcommand:", sub)
+        returncode = 1
+    except mpexceptions.ExceptionUndefinedSamplesDir:
+        print("The env var 'pyglet_mp_samples_dir' is not defined")
+        returncode = 1
+    except mpexceptions.ExceptionSamplesDirDoesNotExist as ex:
+        print("The env var 'pyglet_mp_samples_dir' does not point to an existing directory")
+        returncode = 1
+    except mpexceptions.ExceptionNoSessionIsActive:
+        print("*** Error, no session is active.")
+        returncode = 1
+    except mpexceptions.ExceptionNoSessionWithThatName:
+        print("*** Error, no session by that name")
+        returncode = 1
+    except mpexceptions.ExceptionSessionExistWithSameName:
+        print("*** Error, session exists")
+        returncode = 1
+    except mpexceptions.ExceptionPlaylistFileDoNotExists:
+        print("*** Error, playlist_file does not exist")
+        returncode = 1
+    except mpexceptions.ExceptionBadSampleInPlaylist as ex:
+        print("*** Error, bad sample(s) name in playlist (bad extension or non-existent):")
+        for sample in ex.rejected:
+            print("\t%s" % sample)
+        returncode = 1
+
+    sys.exit(returncode)
+
+if __name__ == "__main__":
+    dispatch_subcommand("configure.py")
diff --git a/tools/ffmpeg/extractors.py b/tools/ffmpeg/extractors.py
new file mode 100644
index 0000000..ed9ca93
--- /dev/null
+++ b/tools/ffmpeg/extractors.py
@@ -0,0 +1,173 @@
+"""
+Responsabilities
+
+Containers with helper functions to analyze media_player recording(s)
+
+Commands 'compare' and 'summarize' uses them to build its reports.
+
+"""
+
+import os
+
+import fs
+
+
+class OverallDefectsCounter:
+    """Helper class to count defects over a range of media player sample plays
+
+    Collaborates with a caller, which determines which samples to process and
+    which samples to skip.
+
+    The main result is a dict of defect: count over all suitable samples
+
+    Additionally, classifies the samples in the disjoint sets:
+        crashed_samples: samples that crashed
+        no_dbg_samples: samples with no media_player state recording
+        skipped_samples: samples that the caller excludes from the overall counts
+        perfect_play_samples: non-skipped samples that played perfectly
+        counters_non_perfect_play_samples: A dict of
+            sample: <counters for each defect type found in recording>
+            for each sample with dbg, no crash, no perfect, not skipped.
+
+    The overall defect counters only uses the samples in the last group.
+
+    Used to:
+        - calculate a session quality score (crashes, defects per sample)
+        - decide which of two session behaves better
+
+    Notice that the more samples are discarded, the less significant will be
+    the score; a good report will need to consider that.
+
+    design notes
+        - this is a bit overcomplicated for single session score, but
+          probably the minimum while comparing two sessions.
+    """
+
+    def __init__(self, pathserv, fn_count_bads):
+        self.pathserv = pathserv
+        self.fn_count_bads = fn_count_bads
+        self.crashed_samples = set()
+        self.no_dbg_samples = set()
+        self.perfect_play_samples = set()
+        self.counters_non_perfect_play_samples = {}
+        self.skipped_samples = set()
+        self.overall_counters = dict()
+        self.overall_counters["scheduling_in_past"] = 0
+        self.last_count = None
+
+    def count_defects(self, sample):
+        dbg_file = self.pathserv.dbg_filename(sample)
+        if not os.path.isfile(dbg_file):
+            self.no_dbg_samples.add(sample)
+            classifies_as = "no_dbg"
+            counters = None
+        else:
+            log_entries = fs.pickle_load(dbg_file)
+            counters = self.fn_count_bads(log_entries)
+            if counters["crash"]:
+                self.crashed_samples.add(sample)
+                classifies_as = "crash"
+            else:
+                perfect = all((counters[k] == 0 for k in counters))
+                classifies_as = "perfect" if perfect else "non_perfect"
+        self.last_count = sample, classifies_as, counters
+        return classifies_as
+
+    def add_count(self, skip=False):
+        sample, classifies_as, counters = self.last_count
+        self.last_count = None
+        if classifies_as in {"no_dbg", "crash"}:
+            return
+        if skip:
+            self.skipped_samples.add(sample)
+            return
+        if classifies_as == "perfect":
+            self.perfect_play_samples.add(sample)
+        else:
+            self.counters_non_perfect_play_samples[sample] = counters
+            for k in counters:
+                self.overall_counters[k] = self.overall_counters.get(k, 0) + counters[k]
+
+    def sum_overall(self):
+        total = 0
+        for k in self.overall_counters:
+            total += self.overall_counters[k]
+        return total
+
+    def total_relevant_samples(self):
+        total = (len(self.perfect_play_samples) +
+                 len(self.counters_non_perfect_play_samples))
+        return total
+
+    def defects_per_sample(self):
+        return self.sum_overall() / self.total_relevant_samples()
+
+
+def single_session_overall_defect_counters(pathserv, fn_count_bads):
+    overall = OverallDefectsCounter(pathserv, fn_count_bads)
+    samples = {e[0] for e in pathserv.session_playlist_generator()}
+    for sample in samples:
+        overall.count_defects(sample)
+        overall.add_count()
+    return overall
+
+
+class CollectSessionComparisonData:
+    """Collects data to compare two sessions
+
+    Constructs the sets samples, other_samples, common_samples, no_dbg_samples,
+    crashed_samples, compared_samples
+
+    For each session calculates an OverallDefectsCounter instance over the set
+    compared_samples; results stored in overall_counters and
+    overall_counters_other.
+    """
+    def __init__(self, pathserv, pathserv_other, fn_count_bads):
+        self.pathserv = pathserv
+        self.pathserv_other = pathserv_other
+
+        # intersection playlist
+        samples = {e for e in fs.load_session_playlist(pathserv)}
+        other_samples = {e for e in fs.load_session_playlist(pathserv_other)}
+        common_samples = samples & other_samples
+
+        # count defects, only for samples with no crash in both sessions
+        no_dbg_samples = set()
+        crashed_samples = set()
+        compared_samples = set()
+        cls = OverallDefectsCounter
+        overall_counters = cls(pathserv, fn_count_bads)
+        overall_counters_o = cls(pathserv_other, fn_count_bads)
+        for sample in common_samples:
+            perfect, counters = overall_counters.count_defects(sample)
+            perfect_o, counters_o = overall_counters_o.count_defects(sample)
+            if counters["no_dbg"] or counters_o["no_dbg"]:
+                no_dbg_samples.add(sample)
+                continue
+            if counters["crash"] or counters_o["crash"]:
+                crashed_samples.add(sample)
+                continue
+            compared_samples.add(sample)
+            overall_counters.count_defects(sample)
+            overall_counters_o.count_defects(sample)
+
+        self.no_dbg_samples = no_dbg_samples
+        self.samples = samples
+        self.other_samples = other_samples
+        self.common_samples = common_samples
+        self.crashed_samples = crashed_samples
+        self.compared_samples = compared_samples
+        self.overall_counters = overall_counters
+        self.overall_counters_other = overall_counters_o
+
+    def confidence_guess(self):
+        """estimates how meaningful the defects per sample are
+
+        The value will be between 1(most meaningful) and 0(useless).
+
+        It follows the heuristic 'the more samples are discarded, the less
+        meaning will be the conclusions draw'
+        """
+        maxz = max(len(self.samples), len(self.other_samples))
+        c = len(self.compared_samples) / maxz if maxz > 0 else 0
+        return c
diff --git a/tools/ffmpeg/fs.py b/tools/ffmpeg/fs.py
new file mode 100644
index 0000000..22b109c
--- /dev/null
+++ b/tools/ffmpeg/fs.py
@@ -0,0 +1,394 @@
+"""
+Responsabilities
+
+Path building for entities into a session should be delegated to fs.PathServices
+Session's creation, activation and management at start of fs
+Versions capture are handled at start of module fs
+Utility functions to load - save at the end of fs
+
+See directory layout in manual.
+"""
+import sys
+import json
+import os
+import pickle
+import shutil
+import subprocess
+
+import mpexceptions
+
+def get_media_player_path():
+    here = os.path.basename(os.path.abspath(__file__))
+    path = os.path.join("..", "..", "examples")
+    mp_path = os.path.abspath(path)
+    return mp_path
+
+# available sessions, activation related functionality #######################
+
+
+def activation_set_to(session):
+    """
+    exceptions:
+        mpexceptions.ExceptionUndefinedSamplesDir
+        mpexceptions.ExceptionNoSessionWithThatName
+        mpexceptions.ExceptionSamplesDirDoesNotExist
+    """
+    samples_dir = get_current_samples_dir()
+    activation_file = get_activation_filename(samples_dir)
+    if session is None:
+        if os.path.isfile(activation_file):
+            os.remove(activation_file)
+    else:
+        json_save(session, activation_file)
+
+
+def get_current_samples_dir():
+    if "pyglet_mp_samples_dir" not in os.environ:
+        raise mpexceptions.ExceptionUndefinedSamplesDir()
+    path = os.environ["pyglet_mp_samples_dir"]
+    if not os.path.isdir(path):
+        raise mpexceptions.ExceptionSamplesDirDoesNotExist(path)
+    return path
+
+
+def get_path_info_for_active_session():
+    samples_dir = get_current_samples_dir()
+    activation_file = get_activation_filename(samples_dir)
+    try:
+        session = json_load(activation_file)
+    except FileNotFoundError:
+        raise mpexceptions.ExceptionNoSessionIsActive()
+    pathserv = PathServices(samples_dir, session)
+    return pathserv
+
+
+def get_path_info_for_session(session):
+    """
+    samples_dir comes from env var pyglet_mp_samples_dir
+    """
+    samples_dir = get_current_samples_dir()
+    pathserv = PathServices(samples_dir, session)
+    conf_file = pathserv.configuration_filename()
+    if os.path.isdir(pathserv.session_dir) and os.path.isfile(conf_file):
+        return pathserv
+    raise mpexceptions.ExceptionNoSessionWithThatName()
+
+
+def get_sessions(samples_dir):
+    candidates = [s for s in os.listdir(samples_dir)
+                  if os.path.isdir(os.path.join(samples_dir, s))]
+    sessions = []
+    for name in candidates:
+        pathserv = PathServices(samples_dir, name)
+        if os.path.isfile(pathserv.configuration_filename()):
+            sessions.append(name)
+    return sessions
+
+
+def get_activation_filename(samples_dir):
+    return os.path.join(samples_dir, "activation.json")
+
+
+def new_session(session, playlist_file=None):
+    """creates a new session and sets as the active session"""
+    samples_dir = get_current_samples_dir()
+    new_session_for_samples_dir(session, samples_dir, playlist_file)
+    activation_set_to(session)
+
+
+def new_session_for_samples_dir(session, samples_dir, playlist_file=None):
+    """creates a new session and returns it's pathserv, session is not activated
+
+    Does not use env vars.
+    """
+    if not os.path.isdir(samples_dir):
+        raise mpexceptions.ExceptionSamplesDirDoesNotExist(samples_dir)
+    pathserv = PathServices(samples_dir, session)
+    if os.path.exists(pathserv.session_dir):
+        raise mpexceptions.ExceptionSessionExistWithSameName()
+    conf = default_initial_configuration()
+
+    if playlist_file is not None:
+        if not os.path.isfile(playlist_file):
+            raise mpexceptions.ExceptionPlaylistFileDoNotExists()
+        conf["has_playlist"] = True
+        samples = sane_samples_from_playlist(pathserv, playlist_file)
+    else:
+        samples = [sample for sample, fname in pathserv.playlist_generator_all()]
+
+    # directory creation delayed to last possible moment so exceptions don't
+    # left empty session dir
+    for path in pathserv.dirs_to_create():
+        os.mkdir(path)
+
+    dump_pyglet_info(pathserv)
+    copy_samples_version_to_dbg(pathserv)
+    save_session_playlist(pathserv, samples)
+    json_save(conf, pathserv.configuration_filename())
+    dump_hg_changeset(pathserv)
+    return pathserv
+
+
+def dump_pyglet_info(pathserv):
+    import pyglet
+    import pyglet.info
+    filename = pathserv.special_raw_filename("pyglet_info")
+    old = sys.stdout
+    try:
+        with open(filename, "w", encoding="utf-8") as f:
+            sys.stdout = f
+            pyglet.info.dump()
+    except Exception as ex:
+        import traceback
+        traceback.print_exc()
+    finally:
+        sys.stdout = old
+
+
+# assumes the cwd hits into pyglet clone under test
+def dump_hg_changeset(pathserv):
+    fname = pathserv.special_raw_filename("pyglet_hg_revision")
+    # win needs shell=True to locate the 'hg'
+    shell = (sys.platform == "win32")
+    with open(fname, "w", encoding="utf8") as outfile:
+        subprocess.call(["hg", "parents", "--encoding", "utf8"],
+                    shell=shell,
+                    stdout=outfile,
+                    timeout=5.0)
+
+
+def copy_samples_version_to_dbg(pathserv):
+    src = pathserv.samples_version_filename()
+    dst = pathserv.special_raw_filename("samples_version")
+    shutil.copyfile(src, dst)
+
+
+def sane_samples_from_playlist(pathserv, playlist_file):
+    # files exist and extension is not blacklisted
+    samples = []
+    rejected = []
+    for sample, filename in pathserv.playlist_generator_from_file(playlist_file):
+        ext = os.path.splitext(sample)[1]
+        if (ext in {".dbg", ".htm", ".html", ".json",
+                    ".log", ".pkl", ".py", ".txt"} or
+            not os.path.isfile(filename)):
+            rejected.append(sample)
+        else:
+            samples.append(sample)
+    if len(rejected) > 0:
+        raise mpexceptions.ExceptionBadSampleInPlaylist(rejected)
+    return samples
+
+
+def save_session_playlist(pathserv, samples):
+    outfile = pathserv.special_raw_filename("session_playlist")
+    text = "\n".join(sorted(samples))
+    txt_save(text, outfile)
+
+
+def load_session_playlist(pathserv):
+    infile = pathserv.special_raw_filename("session_playlist")
+    text = txt_load(infile)
+    lines = text.split("\n")
+    samples = [s.strip() for s in lines]
+    return samples
+
+
+# session configuration functionality #########################################
+
+
+def default_initial_configuration():
+    conf = {
+        "config_vs": "1.0",
+        "protect_raw_data": False,
+        "protect_reports": False,
+        "dev_debug": False,
+        "has_playlist": False,
+        }
+    return conf
+
+
+def update_active_configuration(modify):
+    """updates the active session configuration file with the key: values in modify"""
+    pathserv = get_path_info_for_active_session()
+    update_configuration(pathserv, modify)
+
+
+# validation over modify keys and values is a caller responsibility,
+# here only verified that if key in modify then key in conf
+def update_configuration(pathserv, modify):
+    """The session associated with pathserv gets it's configuration updated
+    with the key: values in modify"""
+    conf = json_load(pathserv.configuration_filename())
+    for k in modify:
+        assert k in conf
+    conf.update(modify)
+    json_save(conf, pathserv.configuration_filename())
+
+
+def get_session_configuration(pathserv):
+    conf = json_load(pathserv.configuration_filename())
+    return conf
+
+
+# path functionality according to path schema ##################################
+
+
+class PathServices:
+    def __init__(self, samples_dir, session):
+        self.samples_dir = samples_dir
+        self.session = session
+        self.session_dir = os.path.join(samples_dir, session)
+
+        self.dbg_dir = os.path.join(self.session_dir, "dbg")
+        self.rpt_dir = os.path.join(self.session_dir, "reports")
+
+    def dirs_to_create(self):
+        return [self.session_dir, self.dbg_dir, self.rpt_dir]
+
+    def sample(self, filename):
+        """returns sample name from filename
+
+        dev note: no checks performed, maybe it should check
+           - filename point into the sample_dir directory
+           - filename exists
+        """
+        return os.path.basename(filename)
+
+    def filename(self, sample):
+        """Returns the filename for the sample
+
+        dev note: no checks performed, maybe it should check
+           - filename exists
+        """
+        return os.path.join(self.samples_dir, sample)
+
+    def dbg_filename(self, sample):
+        """Returns filename to store media_player's internal state recording for sample"""
+        return os.path.join(self.dbg_dir, sample + ".dbg")
+
+    def sample_from_dbg_filename(self, filename):
+        return os.path.splitext(filename)[0]
+
+    def samples_version_filename(self):
+        return os.path.join(self.samples_dir, "_version.txt")
+
+    def report_filename(self, sample, report_name, txt=True):
+        """Returns filename to store a 'per sample' report for sample"""
+        if txt:
+            s = os.path.join(self.rpt_dir, "%s.%s.txt" % (sample, report_name))
+        else:
+            s = os.path.join(self.rpt_dir, "%s.%s" % (sample, report_name))
+        return s
+
+    def special_report_filename(self, report_name):
+        """Returns filename to store a report that don't depend on a single sample"""
+        table = {
+            # report_name: shortname
+            "summary": "00_summary.txt",
+            "pyglet_info": "00_pyglet_info.txt",
+            "pyglet_hg_revision": "04_pyglet_hg_revision.txt"
+            }
+        return os.path.join(self.rpt_dir, table[report_name])
+
+    def special_raw_filename(self, shortname):
+        table = {
+            "session_playlist": "_session_playlist.txt",
+            "crashes_light": "_crashes_light.pkl",
+            "pyglet_info": "_pyglet_info.txt",
+            "samples_version": "_samples_version.txt",
+            "pyglet_hg_revision": "_pyglet_hg_revision.txt"
+            }
+        return os.path.join(self.dbg_dir, table[shortname])
+
+    def configuration_filename(self):
+        return os.path.join(self.session_dir, "configuration.json")
+
+    def playlist_generator(self, playlist_text):
+        lines = playlist_text.split("\n")
+        for line in lines:
+            sample = line.strip()
+            if sample == "" or sample.startswith("#"):
+                continue
+            yield (sample, self.filename(sample))
+
+    def playlist_generator_from_file(self, playlist_file):
+        with open(playlist_file, "r", encoding="utf-8") as f:
+            text = f.read()
+        gen = self.playlist_generator(text)
+        yield from gen
+
+    def playlist_generator_all(self):
+        for name in os.listdir(self.samples_dir):
+            filename = os.path.join(self.samples_dir, name)
+            ext = os.path.splitext(name)[1]
+            if (ext in {".dbg", ".htm", ".html", ".json",
+                        ".log", ".pkl", ".py", ".txt"} or
+                os.path.isdir(filename)):
+                continue
+            yield (name, filename)
+
+    def playlist_generator_from_samples_iterable(self, samples):
+        for sample in samples:
+            yield (sample, self.filename(sample))
+
+    def playlist_generator_from_fixed_text(self):
+        text = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\nz"
+        gen = self.playlist_generator(text)
+        yield from gen
+
+    def session_playlist_generator(self):
+        playlist_file = self.special_raw_filename("session_playlist")
+        gen = self.playlist_generator_from_file(playlist_file)
+        yield from gen
+
+    def session_exists(self):
+        filename = self.configuration_filename()
+        return os.path.isdir(self.session_dir) and os.path.isfile(filename)
+
+
+# fs io #######################################################################
+
+
+def json_save(obj, filename):
+    with open(filename, "w", encoding="utf-8") as f:
+        json.dump(obj, f)
+
+
+def json_load(filename):
+    with open(filename, "r", encoding="utf-8") as f:
+        obj = json.load(f)
+    return obj
+
+
+def pickle_save(obj, filename):
+    with open(filename, "wb") as f:
+        pickle.dump(obj, f)
+
+
+def pickle_load(filename):
+    with open(filename, "rb") as f:
+        obj = pickle.load(f)
+    return obj
+
+
+def txt_load(filename):
+    with open(filename, "r", encoding="utf-8") as f:
+        text = f.read()
+    return text
+
+
+def txt_save(text, filename):
+    with open(filename, "w", encoding="utf-8") as f:
+        f.write(text)
+
+
+def test_composition():
+    text = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\nz"
+    # samples_dir, session
+    args = "aaa", "bbb"
+    a = PathServices(*args)
+    for u, v in zip(a.playlist_generator(text), a.playlist_generator_from_fixed_text()):
+        print(u, v)
+
+#test_composition()
diff --git a/tools/ffmpeg/mp.py b/tools/ffmpeg/mp.py
new file mode 100644
index 0000000..886d74e
--- /dev/null
+++ b/tools/ffmpeg/mp.py
@@ -0,0 +1,4 @@
+"""alias for configure.py"""
+import configure
+
+configure.dispatch_subcommand("mp.py")
diff --git a/tools/ffmpeg/mpexceptions.py b/tools/ffmpeg/mpexceptions.py
new file mode 100644
index 0000000..eb26f6b
--- /dev/null
+++ b/tools/ffmpeg/mpexceptions.py
@@ -0,0 +1,43 @@
+
+class ExceptionPygletMediaPlayerTesting(Exception):
+    pass
+
+class ExceptionUndefinedSamplesDir(ExceptionPygletMediaPlayerTesting):
+    pass
+
+class ExceptionSamplesDirDoesNotExist(ExceptionPygletMediaPlayerTesting):
+    pass
+
+class ExceptionSessionExistWithSameName(ExceptionPygletMediaPlayerTesting):
+    pass
+
+class ExceptionNoSessionIsActive(ExceptionPygletMediaPlayerTesting):
+    pass
+
+class ExceptionNoSessionWithThatName(ExceptionPygletMediaPlayerTesting):
+    pass
+
+class ExceptionAttemptToBreakRawDataProtection(ExceptionPygletMediaPlayerTesting):
+    pass
+
+class ExceptionAttemptToBrekReportsProtection(ExceptionPygletMediaPlayerTesting):
+    pass
+
+class ExceptionPlaylistFileDoNotExists(ExceptionPygletMediaPlayerTesting):
+    pass
+
+class ExceptionBadSampleInPlaylist(ExceptionPygletMediaPlayerTesting):
+    def __init__(self, rejected):
+        self.rejected = rejected
+        super(ExceptionBadSampleInPlaylist, self).__init__()
+
+class ExceptionUnknownReport(ExceptionPygletMediaPlayerTesting):
+    def __init__(self, rpt_name):
+        self.rpt_name = rpt_name
+
+class ExceptionNoDbgFilesPresent(ExceptionPygletMediaPlayerTesting):
+    pass
+
+class ExceptionUnknownOutputFormat(ExceptionPygletMediaPlayerTesting):
+    def __init__(self, output_format):
+        self.output_format = output_format
diff --git a/tools/ffmpeg/playmany.py b/tools/ffmpeg/playmany.py
new file mode 100644
index 0000000..7f20534
--- /dev/null
+++ b/tools/ffmpeg/playmany.py
@@ -0,0 +1,115 @@
+"""
+Usage
+
+    playmany.py
+
+Uses media_player to play a sequence of samples and record debug info
+
+A configuration must be active, see command configure.py
+If the active configuration has disallowed dbg overwrites it will do nothing.
+
+If a playlist was provided at session creation, then only the samples in the
+playlist will be played, otherwise all files in samples_dir.
+
+"""
+
+import os
+import subprocess
+import sys
+
+import fs
+import mpexceptions
+
+
+def main():
+    try:
+        pathserv = fs.get_path_info_for_active_session()
+    except mpexceptions.ExceptionUndefinedSamplesDir:
+        print("The env var 'pyglet_mp_samples_dir' is not defined.")
+        return 1
+    except mpexceptions.ExceptionNoSessionIsActive:
+        print("*** Error, no session active.")
+        return 1
+
+    try:
+        play_many(pathserv, timeout=120)
+    except mpexceptions.ExceptionAttemptToBreakRawDataProtection:
+        print("*** Error, attempt to overwrite raw data when protect_raw_data is True.")
+        return 1
+
+    return 0
+
+
+def play_many(pathserv, timeout=120):
+    """plays the samples in the session playlist for the current active session
+       timeout: max time allowed to play a sample, default is 120 seconds
+    """
+    conf = fs.get_session_configuration(pathserv)
+    if conf["dev_debug"]:
+        pass
+    else:
+        if conf["protect_raw_data"]:
+            raise mpexceptions.ExceptionAttemptToBreakRawDataProtection()
+
+    playlist_gen = pathserv.session_playlist_generator()
+    core_play_many(pathserv, playlist_gen, timeout=timeout)
+
+
+def core_play_many(pathserv, playlist_gen, timeout=120):
+    for sample, filename in playlist_gen:
+        dbg_file = pathserv.dbg_filename(sample)
+
+        print("playmany playing:", filename)
+
+        cmdline = [os.path.join(fs.get_media_player_path(), "media_player.py"),
+                   "--debug",
+                   "--outfile=" + dbg_file,
+                   filename]
+        killed, returncode = cmd__py3(cmdline, timeout=timeout)
+        if killed:
+            print("WARNING: killed by timeout, file: %s" % filename)
+
+
+def cmd__py3(cmdline, bufsize=-1, cwd=None, timeout=60):
+    """runs a .py script as a subprocess with the same python as the caller
+
+       cmdline: list [<scriptname>, arg1, ...]
+       timeout: time in seconds; subprocess wil be killed if it is still running
+                at that time.
+    """
+    # use the same python as the caller to run the script
+    cmdline.insert(0, "-u")
+    cmdline.insert(0, sys.executable)
+
+    p = subprocess.Popen(
+        cmdline,
+        bufsize = bufsize,
+        shell   = False,
+        stdout  = subprocess.PIPE,
+        stderr  = subprocess.PIPE,
+        cwd     = cwd
+    )
+    killed = True
+    try:
+        out, err = p.communicate(timeout=timeout)
+        killed = False
+    except subprocess.TimeoutExpired:
+        p.kill()
+        out, err = p.communicate()
+##    print("out:", out)
+##    print("err:", err)
+
+    returncode = p.returncode
+
+    return killed, returncode
+
+
+def sysargs_to_mainargs():
+    """builds main args from sys.argv"""
+    if len(sys.argv) > 1 and sys.argv[1].startswith("--help"):
+        print(__doc__)
+        sys.exit(1)
+
+if __name__ == "__main__":
+    sysargs_to_mainargs()
+    main()
diff --git a/tools/ffmpeg/readme_ffmpeg_debbuging_branch.txt b/tools/ffmpeg/readme_ffmpeg_debbuging_branch.txt
new file mode 100644
index 0000000..411b494
--- /dev/null
+++ b/tools/ffmpeg/readme_ffmpeg_debbuging_branch.txt
@@ -0,0 +1,33 @@
+This changeset starts from dangillet/pyglet, branch ffmpeg
+	changeset:   3621:42a8441d7950
+	branch:      ffmpeg
+	tag:         tip
+	user:        Daniel Gillet <dan.gillet737@gmail.com>
+	date:        Tue Apr 11 10:39:59 2017 +0200
+	files:       pyglet/media/drivers/openal/adaptation.py
+	description:
+	Fix bug in OpenAL
+	
+( https://bitbucket.org/dangillet/pyglet )
+
+
+Goals
+
+ - better capture of debbuging info
+	More player state captured, less perturbation in timings by capturing raw info and save after play
+
+- support postprocessing of debuggging info into different views / reports
+    Player state captured can be postprocessed in a varity of reports; easy to define new reports.
+	Reports can be rendered for raw data captured in another machine.
+
+- have some quality measure to programatically compare two test runs and tell which is better
+    Rought sketch ATM, qtty of anomalies over all samples in the debbuging session.
+	Useful to tell if a change in code or library gives better or worse results.
+
+- enforce an ordered schema to store test results
+	Avoids mixing results obtained under different settings.
+	Consistent access to debug data captured by different persons or under different conditions.
+
+See readme_run_test.txt for the instructions a tester or user will need to follow to send debug info.
+
+See the manual.txt initial sections for dev workflows and basic descriptions.
diff --git a/tools/ffmpeg/readme_run_tests.txt b/tools/ffmpeg/readme_run_tests.txt
new file mode 100644
index 0000000..e3b5f02
--- /dev/null
+++ b/tools/ffmpeg/readme_run_tests.txt
@@ -0,0 +1,57 @@
+Running the tests
+=================
+
+preparation
+-----------
+
+1. You need to have ffmpeg binaries to get the dynamic link libraries, 
+Windows:
+Download the binaries from http://ffmpeg.zeranoe.com/builds/ ; that page lets select Version / Architecture / Linking
+We need to select
+	'shared' to get the dll's
+	'Architecture' according to the target machine (32/64bits)
+	'Version' I suggest the released, currently 3.3.1 (the other option seems to be a nightly build, complicates comparison  between machines)
+After download, unpack at some directory, by example prog/ffmpeg; then copy the dll's in prog/ffmpeg/bin to the directory with the pyglet ffmpeg player and test script; currently (pyglet repo)/examples/video_ffmpeg
+
+Ubuntu:
+Try in a terminal 'ffmpeg'; if it tells is not installed you can get it with
+	sudo apt install ffmpeg
+For ubuntu 17.04 I got version 3.2.4-1build2
+(note: if a sample looks bad in the pyglet player, you could run 
+	ffplay.exe <sample>
+in a console to see if it should play well)
+
+
+Other OS: please add the relevant info.
+
+2. You need to have the samples, download the .zip with highest version available in
+https://www.dropbox.com/sh/1hz8lwy5utmg4p7/AADVEUEfKqPqlbizVMSBP4nHa?dl=0
+
+Unzip the samples, better if outside the pyglet repo clone.
+
+3. Ensure your local pyglet copy is at branch ffmpeg
+	cd repo_dir
+	hg update ffmpeg
+	
+4. ensure the pyglet in repo_dir is seen by python
+
+Preparation is complete.
+
+Running the tests
+-----------------
+
+	cd repo_dir/examples/video_ffmpeg
+	python3 run_test_suite.py <samples dir>
+	
+Each sample will be played and the media_player state along the play will be recorded.
+The raw info collected would be in <samples_dir>\testun_00\dbg (or _01, _02 ... for subsequent runs)
+This is the info a developer may ask when troubleshooting an issue, it also includes info about the OS, python version, pyglet version.
+
+Additionally, a preliminary results analysis is writen to <samples_dir>/testrun_00/reports
+
+For more detail and aptions look at the manual.
+
+Windows note: In one machine has been observed that for each sample run it pop-ups an OS Message window with "C:\Windows\perf.dll not designed to run in Windows or it contains an error ..."; pressing the ok button will continue without noticeable problems. The same machine was running ok at some time; after a bunch of non-pyglet software updates this problem appeared. I will need some time to investigate this, but other than the anoying button click it seems to not cause problems.
+
+Linux note: In one machine with Ubuntu 17.04 has been observed that for some testruns it pop-ups an OS Message window with "python3.5 crashed with SIGABRT in pa_mainloop_dispatch()", pressing the ok button testing continues at next sample.
+Not always the same sample. Looks as a race condition, probably involving the code that sets a callback, because repeatdly playing a sample with media_player the console will eventually show a traceback with "Assertion 'c->callback' failed at pulsecore/socket-client.c:126, function do_call(). Aborting". The test_suite will retry to play crashed samples upto 5 times to get a clean debug reporting.
diff --git a/tools/ffmpeg/report.py b/tools/ffmpeg/report.py
new file mode 100644
index 0000000..6ba897d
--- /dev/null
+++ b/tools/ffmpeg/report.py
@@ -0,0 +1,66 @@
+import sys
+
+import reports
+import fs
+import mpexceptions
+
+
+def description():
+    template = ("""
+Usage
+
+    report.py sample report_name
+
+Generates a report from the debugging info recorded while playing sample
+
+Arguments
+
+    sample: sample to report
+    report_name: desired report, one of
+{reports}
+
+The report will be written to session's output dir under reports/sample.report_name.txt
+
+Example
+
+    report anomalies small.mp4
+
+will write the report 'anomalies' to report/small.mp4.anomalies.txt
+"""
+                )
+    text = template.format(reports=reports.available_reports_description(line_prefix=" " * 8))
+    return text
+
+
+def main(sample, report_name):
+    try:
+        pathserv = fs.get_path_info_for_active_session()
+    except mpexceptions.ExceptionUndefinedSamplesDir:
+        print("The env var 'pyglet_mp_samples_dir' is not defined.")
+        return 1
+    except mpexceptions.ExceptionNoSessionIsActive:
+        print("*** Error, no session active.")
+        return 1
+
+    report_sample(pathserv, sample, report_name)
+
+
+def report_sample(pathserv, sample, report_name):
+    dbg_file = pathserv.dbg_filename(sample)
+    outfile = pathserv.report_filename(sample, report_name)
+    log_entries = fs.pickle_load(dbg_file)
+    text = reports.report_by_name(report_name)(log_entries)
+    fs.txt_save(text, outfile)
+
+
+def sysargs_to_mainargs():
+    """builds main args from sys.argv"""
+    if len(sys.argv) < 3:
+        print(description())
+        sys.exit(1)
+    report_name, sample = sys.argv[1:]
+    return report_name, sample
+
+if __name__ == '__main__':
+    sample, report_name = sysargs_to_mainargs()
+    main(sample, report_name)
diff --git a/tools/ffmpeg/reports.py b/tools/ffmpeg/reports.py
new file mode 100644
index 0000000..eb3d5e8
--- /dev/null
+++ b/tools/ffmpeg/reports.py
@@ -0,0 +1,182 @@
+import os
+
+import fs
+from pyglet.media import instrumentation as ins
+import mpexceptions
+
+
+def render_event(events_definition, state, fn_prefix):
+    evname = state["evname"]
+    ev_description = events_definition[evname]
+    parts = [fn_prefix(state)]
+    parts.append(ev_description["desc"])
+    for name in ev_description["update_names"]:
+        if name == "evname":
+            continue
+        parts.append(" %s: %s" % (name, state[name]))
+    extra_names = ev_description["other_fields"]
+    if extra_names:
+        parts.append(' ;')
+        for name in extra_names:
+            parts.append(" %s: %s" % (name, state[name]))
+    text = "".join(parts)
+    return text
+
+
+def rpt_all(log_entries, events_definition=ins.mp_events):
+    no_indent = {"crash", "mp.im", "p.P._sp", "p.P.sk", "p.P.ut.1.0", "p.P.oe"}
+    fn_prefix = lambda st: "" if st["evname"] in no_indent else "    "
+    mp_states = ins.MediaPlayerStateIterator(log_entries, events_definition)
+    parts = []
+    for mp in mp_states:
+        s = "%4d " % mp["frame_num"] + render_event(events_definition, mp, fn_prefix)
+        parts.append(s)
+    text = "\n".join(parts)
+    return text
+
+
+def rpt_anomalies(log_entries, events_definition=ins.mp_events):
+    allow = {"crash", "mp.im", "p.P.sk", "p.P.ut.1.3", "p.P.ut.1.4",
+             "p.P.ut.1.5", "p.P.ut.1.7", "p.P.ut.1.8", "p.P.oe"}
+    mp_states = ins.MediaPlayerStateIterator(log_entries, events_definition)
+    fn_prefix = lambda st: "%4d " % st["frame_num"]
+    parts = []
+    for st in mp_states:
+        if st["evname"] == "p.P.ut.1.9":
+            if st["rescheduling_time"] < 0:
+                parts.append("%4d !!! Scheduling in the past, reschedulling_time: %f" %
+                             (st["frame_num"], st["rescheduling_time"]))
+        elif st["evname"] in allow:
+            parts.append(render_event(events_definition, st, fn_prefix))
+    text = "\n".join(parts)
+    return text
+
+
+def rpt_counter(log_entries, events_definition=ins.mp_events):
+    count_bads = ins.CountBads(events_definition, ins.mp_bads)
+    counters = count_bads.count_bads(log_entries)
+    text = format_anomalies_counter(count_bads.anomalies_description, counters)
+    return text
+
+
+def format_anomalies_counter(anomalies_description, counters, sample=None):
+    parts = []
+    fmt = "%4d %s"
+    for anomaly in counters:
+        if counters[anomaly]:
+            desc = anomalies_description[anomaly]
+            parts.append(fmt % (counters[anomaly], desc))
+    if parts:
+        parts_sorted = sorted(parts, key=lambda e: e[4:])
+        text = "\n".join(parts_sorted)
+        text = ("Counts of anomalies\n" +
+                "qtty anomaly\n" + text)
+    else:
+        text = "No anomalies detected."
+    if sample is not None:
+        text = "Sample: %s\n" % sample + text
+    return text
+
+
+def fragment_crash_retries(pathserv):
+    crashes_light_file = pathserv.special_raw_filename("crashes_light")
+    if not os.path.isfile(crashes_light_file):
+        text = "No crash info available, please run retry_crashes.py"
+        return text
+    total_retries, sometimes_crashed, still_crashing = fs.pickle_load(crashes_light_file)
+    if len(sometimes_crashed) == 0:
+        text = "No sample crashed the player."
+        return text
+    parts = []
+    if still_crashing:
+        parts.append("Samples that crashed in any one of the %d attempts to play" % (total_retries + 1))
+        sorted_keys = sorted(still_crashing)
+        for sample in sorted_keys:
+            parts.append(sample)
+
+    if still_crashing and sometimes_crashed:
+        parts.append("")
+
+    if sometimes_crashed:
+        parts.append("Samples that crashed but finally played entirelly")
+        finally_played = sometimes_crashed - still_crashing
+        if finally_played:
+            sorted_keys = sorted(finally_played)
+            for sample in sorted_keys:
+                parts.append(sample)
+        else:
+            parts.append("<none>")
+
+    text = "\n".join(parts)
+    return text
+
+
+available_reports = {
+    # <report name>: (<report function>, <Short description for user>)
+    "anomalies": (rpt_anomalies, "Start, end and interesting events"),
+    "all": (rpt_all, "All data is exposed as text"),
+    "counter": (rpt_counter, "How many occurrences of each defect")
+    }
+
+
+def available_reports_description(line_prefix):
+    d = available_reports
+    lines = ["%s%s: %s" % (line_prefix, k, d[k][1]) for k in d]
+    lines.sort()
+    text = "\n".join(lines)
+    return text
+
+
+def report_by_name(name):
+    try:
+        fn_report = available_reports[name][0]
+    except KeyError:
+        raise mpexceptions.ExceptionUnknownReport(name)
+    return fn_report
+
+
+def report_session_comparison(session_comparison_data):
+    sd = session_comparison_data
+    header = "crashes | Defects per sample | session"
+    fmt = "%7d | %18.3f | %s"
+    parts = [header]
+
+    cnt_crashes = len(sd.overall_counters.crashed_samples)
+    score = sd.overall_counters.defects_per_sample()
+    session = sd.overall_counters.pathserv.session
+    parts.append(fmt % (cnt_crashes, score, session))
+
+    cnt_crashes_o = len(sd.overall_counters.crashed_samples)
+    score_o = sd.overall_counters_o.defects_per_sample()
+    session_o = sd.overall_counters_o.pathserv.session
+    parts.append(fmt % (cnt_crashes_o, score_o, session_o))
+
+    parts.append("")
+    c = sd.confidence_guess()
+    parts.append("Confidence guess, from 1(highest) to 0(lowest): %.2f" % c)
+
+    parts.append("")
+    parts.append("Summary samples ignored for scores")
+    parts.append("qtty | reason")
+    # playlist
+    n = len(sd.samples - sd.common_samples)
+    parts.append("%4d   only on playlist session '%s'" % (n, session))
+    n = len(sd.other_samples - sd.common_samples)
+    parts.append("%4d   only on playlist session '%s'" % (n, session_o))
+    # dbg
+    n = len(sd.overall_counters.no_dbg_samples & sd.overall_counters_other.no_dbg_samples)
+    parts.append("%4d   no .dbg in both sessions" % n)
+    n = len(sd.overall_counters.no_dbg_samples - sd.overall_counters_other.no_dbg_samples)
+    parts.append("%4d   .dbg missing only on session '%s'" % (n, session))
+    n = len(sd.overall_counters_other.no_dbg_samples - sd.overall_counters.no_dbg_samples)
+    parts.append("%4d   .dbg missing only on session '%s'" % (n, session_o))
+    # crashes
+    n = len(sd.overall_counters.crashed_samples & sd.overall_counters_other.crashed_samples)
+    parts.append("%4d   crashed in both sessions" % n)
+    n = len(sd.overall_counters.crashed_samples - sd.overall_counters_other.crashed_samples)
+    parts.append("%4d   crashed only on session '%s'" % (n, session))
+    n = len(sd.overall_counters_other.crashed_samples - sd.overall_counters.crashed_samples)
+    parts.append("%4d   crashed only on session '%s'" % (n, session_o))
+
+    text = "\n".join(parts)
+    return text
diff --git a/tools/ffmpeg/retry_crashed.py b/tools/ffmpeg/retry_crashed.py
new file mode 100644
index 0000000..4646fc2
--- /dev/null
+++ b/tools/ffmpeg/retry_crashed.py
@@ -0,0 +1,107 @@
+"""
+Usage
+
+    retry_crashed.py [--clean] [max_retries]
+
+Inspects the raw data collected to get the list of samples that crashed the last
+time they were played.
+Then it replays those samples, recording new raw data for them.
+
+The process is repeated until all samples has a recording with no crashes or
+the still crashing samples were played 'max_tries' times in this command run.
+
+A configuration must be active, see command configure.py
+
+Besides the updated debug recordings, a state is build and saved:
+    total_retries: total retries attempted, including previous runs
+    sometimes_crashed: list of samples that crashed one time but later completed a play
+    always_crashed: list of samples that always crashed
+
+Options:
+    --clean: discards crash data collected in a previous run
+    max_retries: defaults to 5
+"""
+import os
+import sys
+
+import fs
+from pyglet.media import instrumentation as ins
+import mpexceptions
+import playmany
+
+
+def main(clean, max_retries):
+    try:
+        pathserv = fs.get_path_info_for_active_session()
+    except mpexceptions.ExceptionUndefinedSamplesDir:
+        print("The env var 'pyglet_mp_samples_dir' is not defined.")
+        return 1
+    except mpexceptions.ExceptionNoSessionIsActive:
+        print("*** Error, no session active.")
+        return 1
+
+    if clean:
+        crashes_light_file = pathserv.special_raw_filename("crashes_light")
+        if os.path.isfile(crashes_light_file):
+            os.unlink(crashes_light_file)
+
+    retry_crashed(pathserv, max_retries)
+    return 0
+
+
+def retry_crashed(pathserv, max_retries=5):
+    crashes_light_file = pathserv.special_raw_filename("crashes_light")
+    if os.path.isfile(crashes_light_file):
+        total_retries, sometimes_crashed, still_crashing = fs.pickle_load(crashes_light_file)
+        samples_to_consider = still_crashing
+    else:
+        total_retries, sometimes_crashed = 0, set()
+        playlist_gen = pathserv.session_playlist_generator()
+        samples_to_consider = [s for s, f in playlist_gen]
+
+    cnt_retries = 0
+    while 1:
+        still_crashing = set()
+        for sample in samples_to_consider:
+            if sample_crashed(pathserv, sample):
+                sometimes_crashed.add(sample)
+                still_crashing.add(sample)
+        fs.pickle_save((total_retries, sometimes_crashed, still_crashing), crashes_light_file)
+
+        if cnt_retries >= max_retries or len(still_crashing) == 0:
+            break
+        samples_to_consider = still_crashing
+        playlist = pathserv.playlist_generator_from_samples_iterable(samples_to_consider)
+        playmany.core_play_many(pathserv, playlist)
+        cnt_retries += 1
+        total_retries += 1
+
+
+def sample_crashed(pathserv, sample):
+    dbg_file = pathserv.dbg_filename(sample)
+    log_entries = fs.pickle_load(dbg_file)
+    return ins.crash_detected(log_entries)
+
+
+def sysargs_to_mainargs():
+    """builds main args from sys.argv"""
+    if len(sys.argv) > 1 and sys.argv[1].startswith("--help"):
+        print(__doc__)
+        sys.exit(1)
+
+    if len(sys.argv) > 1 and sys.argv[1] == "--clean":
+        sys.argv.pop(1)
+        clean = True
+    else:
+        clean = False
+
+    if len(sys.argv) > 1:
+        max_retries = int(sys.argv[1])
+    else:
+        max_retries = 5
+
+    return clean, max_retries
+
+if __name__ == "__main__":
+    clean, max_retries = sysargs_to_mainargs()
+    main(clean, max_retries)
diff --git a/tools/ffmpeg/run_test_suite.py b/tools/ffmpeg/run_test_suite.py
new file mode 100644
index 0000000..49c6b04
--- /dev/null
+++ b/tools/ffmpeg/run_test_suite.py
@@ -0,0 +1,103 @@
+"""
+Usage
+
+    run_test_suite.py [samples_dir]
+
+Plays media samples with the pyglet media_player, recording debug information
+for each sample played and writing reports about the data captured.
+
+Arguments
+    samples_dir: directory with the media samples to play
+
+If no sample_dir is provided the samples to play will come from the active
+session (see cmd configure new)
+
+If the samples_dir is provided, all files whitin will be played except for the
+ones with extension ".dbg", ".htm", ".html", ".json", ".log", ".pkl", ".py",
+".txt" ; subdirectories of samples_dir will not be explored. 
+
+Output files will be into
+    samples_dir/testrun/dbg : binary capture of media_player events
+    samples_dir/testrun/reports : human readable reports
+
+(with testrun provided by the active session if sample_dir was not provided)
+"""
+
+import os
+import sys
+
+import fs
+from pyglet.media import instrumentation as ins
+import playmany
+import retry_crashed
+import summarize
+
+
+def main(samples_dir):
+    if samples_dir is None:
+        # get from active session
+        pathserv = fs.get_path_info_for_active_session()
+    else:
+        # create a session with a default name, provide a pathserv
+        session = None
+        for i in range(100):
+            name = "testrun_%02d" % i
+            if not os.path.exists(os.path.join(samples_dir, name)):
+                session = name
+                pathserv = fs.new_session_for_samples_dir(session, samples_dir)
+                break
+        if session is None:
+            s = os.path.join(samples_dir, "testrun_n")
+            print("*** Error, failed to create output dir in the form %s , for any n<100." % s)
+            sys.exit(1)
+
+    print("Results will be write to directory: % s" % pathserv.session_dir)
+
+    playmany.play_many(pathserv)
+
+    retry_crashed.retry_crashed(pathserv)
+
+    count_bads = ins.CountBads()
+    summarize.summarize(pathserv, count_bads)
+
+    # protect raw data and reports
+    modify = {
+        "protect_raw_data": True,
+        "protect_reports": True,
+        }
+    fs.update_configuration(pathserv, modify)
+    
+    print("*** done ***") 
+
+
+def usage():
+    print(__doc__)
+    sys.exit(1)
+
+
+def sysargs_to_mainargs():
+    """builds main args from sys.argv"""
+    if len(sys.argv) > 1:
+        for i in range(1):
+            if sys.argv[1].startswith("--"):
+                a = sys.argv.pop(1)
+                if a.startswith("--help"):
+                    usage()
+                else:
+                    print("Error unknown option:", a)
+                    usage()
+
+    if len(sys.argv) > 1:
+        samples_dir = sys.argv.pop(1)
+    else:
+        samples_dir = None
+
+    if len(sys.argv) > 1:
+        print("Error, unexpected extra params: %s ..." % sys.argv[1])
+        usage()
+
+    return samples_dir 
+
+if __name__ == '__main__':
+    samples_dir = sysargs_to_mainargs()
+    main(samples_dir)
diff --git a/tools/ffmpeg/summarize.py b/tools/ffmpeg/summarize.py
new file mode 100644
index 0000000..0a846df
--- /dev/null
+++ b/tools/ffmpeg/summarize.py
@@ -0,0 +1,147 @@
+"""
+Usage
+
+    summarize.py
+
+Summarizes the session info collected with playmany and retry_crashes
+
+A configuration must be active, see command configure.py
+
+If a playlist was provided at session creation, then only the samples in the
+playlist will be played, otherwise all files in samples_dir.
+"""
+
+import shutil
+import sys
+
+import extractors
+import fs
+from pyglet.media import instrumentation as ins
+import mpexceptions
+import report
+import reports
+
+
+def main():
+    try:
+        pathserv = fs.get_path_info_for_active_session()
+    except mpexceptions.ExceptionUndefinedSamplesDir:
+        print("The env var 'pyglet_mp_samples_dir' is not defined.")
+        return 1
+    except mpexceptions.ExceptionNoSessionIsActive:
+        print("*** Error, no session active.")
+        return 1
+
+    count_bads = ins.CountBads()
+
+    try:
+        summarize(pathserv, count_bads)
+    except mpexceptions.ExceptionAttemptToBrekReportsProtection:
+        print("*** Error, attempt to overwrite reports when protect_reports "
+              "is True.")
+        return 1
+    except mpexceptions.ExceptionNoDbgFilesPresent:
+        print("*** Error, no dbg files present; maybe playmany should be run?")
+        return 1
+    return 0
+
+
+def summarize(pathserv, count_bads):
+    conf = fs.get_session_configuration(pathserv)
+    if conf["dev_debug"]:
+        pass
+    else:
+        if conf["protect_reports"]:
+            raise mpexceptions.ExceptionAttemptToBreakRawDataProtection()
+
+    session = pathserv.session
+    parts = ["Summary of media_player debug info for session: %s" % session]
+    filename = pathserv.special_raw_filename("samples_version")
+    try:
+        text = fs.txt_load(filename)
+    except Exception as ex:
+        print(ex)
+        print("*** Error, could not read dbg/_version.txt, "
+              "target dir probably not a session one.")
+        sys.exit(1)
+    parts.append("Samples version: %s" % text)
+
+    overall = extractors.single_session_overall_defect_counters(pathserv, count_bads.count_bads)
+
+    if len(overall.no_dbg_samples):
+        parts.append("Samples with no .dbg recording: %d" % len(overall.no_dbg_samples))
+    else:
+        parts.append("All samples have a .dbg recording.")
+
+    if len(overall.crashed_samples):
+        parts.append("Samples that always crashed: %d" % len(overall.crashed_samples))
+    else:
+        parts.append("All samples (finally) got a report with no crash.")
+
+    n = overall.total_relevant_samples()
+    parts.append("Relevant samples (all - no_dbg - crashed): %d" % n)
+
+    no_relevant_samples = (n == 0)
+    if no_relevant_samples:
+        parts.append("*** Error, no relevant samples remains to calculate"
+                     " score.")
+    else:
+        parts.extend([
+            "Naive quality number (anomalies / samples): %f" % overall.defects_per_sample(),
+            "Relevant samples with perfect play: %d / %d" % (len(overall.perfect_play_samples), n),
+            "Relevant samples with anomalies   : %d / %d" % (len(overall.counters_non_perfect_play_samples), n),
+            ""
+            ])
+
+    # include crash info collected by retry_crashed.py
+    text = reports.fragment_crash_retries(pathserv)
+    parts.append(text)
+    parts.append("")
+
+    # include count of defects for samples with anomalies but no crash
+    if len(overall.counters_non_perfect_play_samples):
+        parts.append("Per sample defects info (always crashed and perfect plays"
+                     " not listed)\n")
+        for sample in sorted(overall.counters_non_perfect_play_samples.keys()):
+            counters = overall.counters_non_perfect_play_samples[sample]
+            text = reports.format_anomalies_counter(
+                count_bads.anomalies_description, counters, sample)
+            parts.append(text)
+            parts.append("")
+
+        # additionally emit reports 'anomalies' and 'all'
+        for sample in overall.counters_non_perfect_play_samples.keys():
+            report.report_sample(pathserv, sample, "anomalies")
+            report.report_sample(pathserv, sample, "all")
+
+    # appendix, list of samples without dbg
+    if len(overall.no_dbg_samples):
+        parts.append("Appendix, list of samples without dbg")
+        for s in overall.no_dbg_samples:
+            parts.append("\t%s" % s)
+        parts.append("")
+
+    text = "\n".join(parts)
+    filename_summary = pathserv.special_report_filename("summary")
+    fs.txt_save(text, filename_summary)
+
+    # output pyglet info as captured at session creation
+    src = pathserv.special_raw_filename("pyglet_info")
+    dst = pathserv.special_report_filename("pyglet_info")
+    shutil.copyfile(src, dst)
+    src = pathserv.special_raw_filename("pyglet_hg_revision")
+    dst = pathserv.special_report_filename("pyglet_hg_revision")
+    try:
+        shutil.copyfile(src, dst)
+    except Exception:
+        pass
+
+
+def sysargs_to_mainargs():
+    """builds main args from sys.argv"""
+    if len(sys.argv) > 1 and sys.argv[1].startswith("--help"):
+        print(__doc__)
+        sys.exit(1)
+
+if __name__ == '__main__':
+    main()
diff --git a/tools/ffmpeg/test_instrumentation.py b/tools/ffmpeg/test_instrumentation.py
new file mode 100644
index 0000000..d6d07dc
--- /dev/null
+++ b/tools/ffmpeg/test_instrumentation.py
@@ -0,0 +1,35 @@
+from pyglet.media import instrumentation as ins
+
+def test_no_unknown_state_fields_in_mp_events():
+    all_fields = ins.MediaPlayerStateIterator.fields.keys()
+    ok = True
+    for evname in ins.mp_events:
+        if evname == "version":
+            continue
+        for name in ins.mp_events[evname]["update_names"]:
+            if name not in all_fields:
+                print("Error, in evname '%s' unknown field '%s' in 'update_names'" % (evname, name))
+                ok = False
+        for name in ins.mp_events[evname]["other_fields"]:
+            if name not in all_fields:
+                print("Error, in evname '%s' unknown field '%s' in 'other_fields'" % (evname, name))
+                ok = False
+    if ok:
+        print("test_no_unknown_state_fields_in_mp_events: passed")
+
+def test_evname_in_mp_events_testcases():
+    ok = True
+    for evname in ins.mp_events:
+        if evname == "version":
+            continue
+        for i, args in enumerate(ins.mp_events[evname]["test_cases"]):
+            if evname != args[0]:
+                msg = "Error, for evname %s the testase #%d does not match evname"
+                print(msg % (evname, i))
+                ok = False
+    if ok:
+        print("test_evname_in_mp_events_testcases: passed")
+    
+
+test_no_unknown_state_fields_in_mp_events()
+test_evname_in_mp_events_testcases()
diff --git a/tools/ffmpeg/timeline.py b/tools/ffmpeg/timeline.py
new file mode 100644
index 0000000..5afd602
--- /dev/null
+++ b/tools/ffmpeg/timeline.py
@@ -0,0 +1,98 @@
+"""
+Usage
+
+    timeline.py sample [output_format]
+
+Renders the media player's debug info to a format more suitable to postprocess
+in a spreadsheets or other software, particularly to get a data visualization.
+
+See output details in the manual.
+
+Arguments
+    sample: sample to report
+    output_format : one of { "csv", "pkl"}, by default saves as .pkl (pickle)
+
+The output will be written to session's output dir under
+    reports/sample.timeline.[.pkl or .csv]
+
+Example
+
+    timeline.py small.mp4
+
+will write the output to report/small.mp4.timeline.pkl
+
+NOTE: .csv sample is currently not implemented
+"""
+
+import sys
+
+from pyglet.media import instrumentation as ins
+import fs
+import mpexceptions
+
+
+def usage():
+    print(__doc__)
+    sys.exit(1)
+
+
+def main(sample, output_format):
+    try:
+        pathserv = fs.get_path_info_for_active_session()
+    except mpexceptions.ExceptionUndefinedSamplesDir:
+        print("The env var 'pyglet_mp_samples_dir' is not defined.")
+        return 1
+    except mpexceptions.ExceptionNoSessionIsActive:
+        print("*** Error, no session active.")
+        return 1
+
+    try:
+        save_timeline(pathserv, sample, output_format)
+    except mpexceptions.ExceptionUnknownOutputFormat as ex:
+        print("*** Error, unknown output_format: %s" % output_format)
+    return 0
+
+
+def save_timeline(pathserv, sample, output_format):
+    dbg_file = pathserv.dbg_filename(sample)
+    recorded_events = fs.pickle_load(dbg_file)
+    tm = ins.TimelineBuilder(recorded_events, events_definition=ins.mp_events)
+    timeline = tm.get_timeline()
+    v = ins.timeline_postprocessing(timeline)
+    if output_format == ".pkl":
+        outfile = pathserv.report_filename(sample, "timeline.pkl", False)
+        fs.pickle_save(v, outfile)
+    elif output_format == ".csv":
+        outfile = pathserv.report_filename(sample, "timeline.csv", False)
+        #? todo: investigate packing multiple tables
+        raise NotImplemented
+    else:
+        raise mpexceptions.ExceptionUnknownOutputFormat(output_format)
+
+
+def sysargs_to_mainargs():
+    """builds main args from sys.argv"""
+    if len(sys.argv) < 2:
+        usage()
+    if sys.argv[1].startswith("--"):
+        a = sys.argv.pop(1)
+        if a.startswith("--help"):
+            usage()
+        else:
+            print("Error unknown option:", a)
+            usage()
+
+    if len(sys.argv) < 2:
+        print("*** Error, missing argument.\n")
+        usage()
+    sample = sys.argv.pop(1)
+
+    output_format = ".pkl"
+    if len(sys.argv) > 1:
+        output_format = sys.argv.pop(1)
+
+    return sample, output_format
+
+if __name__ == '__main__':
+    sample, output_format = sysargs_to_mainargs()
+    main(sample, output_format)
diff --git a/tools/gendist.sh b/tools/gendist.sh
new file mode 100755
index 0000000..5a38849
--- /dev/null
+++ b/tools/gendist.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+# $Id$
+
+base=`dirname $0`/..
+cd $base
+
+VERSION=`grep 'VERSION =' setup.py | cut -d "'" -f2`
+
+# Source dists
+python setup.py sdist --formats=zip
+
+# Wheels
+python setup.py bdist_wheel 
+
+# Build docs archive
+rm dist/pyglet-docs-$VERSION.zip
+(cd doc/_build; zip -r docs.zip html)
+mv doc/_build/docs.zip dist/pyglet-docs-$VERSION.zip
+# Add the examples
+zip -r dist/pyglet-docs-$VERSION.zip examples
diff --git a/tools/gengl.py b/tools/gengl.py
new file mode 100644
index 0000000..8434893
--- /dev/null
+++ b/tools/gengl.py
@@ -0,0 +1,234 @@
+#!/usr/bin/env python
+
+'''Generate files in pyglet.gl and pyglet/GLU
+'''
+from __future__ import print_function
+
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+import marshal
+import optparse
+import os.path
+import urllib2
+import sys
+import textwrap
+
+from wraptypes.wrap import CtypesWrapper
+
+script_dir = os.path.abspath(os.path.dirname(__file__))
+
+GLEXT_ABI_H = 'https://www.khronos.org/registry/OpenGL/api/GL/glext.h'
+GLXEXT_ABI_H = 'https://www.khronos.org/registry/OpenGL/api/GL/glxext.h'
+WGLEXT_ABI_H = 'https://www.khronos.org/registry/OpenGL/api/GL/wglext.h'
+GLEXT_NV_H = 'http://developer.download.nvidia.com/opengl/includes/glext.h'
+GLXEXT_NV_H = 'http://developer.download.nvidia.com/opengl/includes/glxext.h'
+WGLEXT_NV_H = 'http://developer.download.nvidia.com/opengl/includes/wglext.h'
+
+AGL_H = '/System/Library/Frameworks/AGL.framework/Headers/agl.h'
+GL_H = '/usr/include/GL/gl.h'
+GLU_H = '/usr/include/GL/glu.h'
+GLX_H = '/usr/include/GL/glx.h'
+WGL_H = os.path.join(script_dir, 'wgl.h')
+
+CACHE_FILE = os.path.join(script_dir, '.gengl.cache')
+_cache = {}
+
+def load_cache():
+    global _cache
+    if os.path.exists(CACHE_FILE):
+        try:
+            _cache = marshal.load(open(CACHE_FILE, 'rb')) or {}
+        except:
+            pass
+    _cache = {}
+
+def save_cache():
+    try:
+        marshal.dump(_cache, open(CACHE_FILE, 'wb'))
+    except:
+        pass
+
+def read_url(url):
+    if url in _cache:
+        return _cache[url]
+    if os.path.exists(url):
+        data = open(url).read()
+    else:
+        data = urllib2.urlopen(url).read()
+    _cache[url] = data
+    save_cache()
+    return data
+
+class GLWrapper(CtypesWrapper):
+    requires = None
+    requires_prefix = None
+
+    def __init__(self, header):
+        self.header = header
+        super(GLWrapper, self).__init__()
+
+    def print_preamble(self):
+        import time
+        print(textwrap.dedent("""
+             # This content is generated by %(script)s.
+             # Wrapper for %(header)s
+        """ % {
+            'header': self.header,
+            'date': time.ctime(),
+            'script': __file__,
+        }).lstrip(), file=self.file)
+
+    def handle_ctypes_function(self, name, restype, argtypes, filename, lineno):
+        if self.does_emit(name, filename):
+            self.emit_type(restype)
+            for a in argtypes:
+                self.emit_type(a)
+
+            self.all_names.append(name)
+            print('# %s:%d' % (filename, lineno), file=self.file)
+            print('%s = _link_function(%r, %s, [%s], %r)' % \
+              (name, name, str(restype), 
+               ', '.join([str(a) for a in argtypes]), self.requires), file=self.file)
+            print(file=self.file)
+
+    def handle_ifndef(self, name, filename, lineno):
+        if self.requires_prefix and \
+           name[:len(self.requires_prefix)] == self.requires_prefix:
+            self.requires = name[len(self.requires_prefix):]
+            print('# %s (%s:%d)'  % \
+                (self.requires, filename, lineno), file=self.file)
+
+def progress(msg):
+    print(msg, file=sys.stderr)
+
+marker_begin = '# BEGIN GENERATED CONTENT (do not edit below this line)\n'
+marker_end = '# END GENERATED CONTENT (do not edit above this line)\n'
+
+class ModuleWrapper(object):
+    def __init__(self, header, filename,
+                 prologue='', requires_prefix=None, system_header=None,
+                 link_modules=()):
+        self.header = header
+        self.filename = filename
+        self.prologue = prologue
+        self.requires_prefix = requires_prefix
+        self.system_header = system_header
+        self.link_modules = link_modules
+
+    def wrap(self, dir):
+        progress('Updating %s...' % self.filename)
+        source = read_url(self.header) 
+        filename = os.path.join(dir, self.filename)
+
+        prologue = []
+        epilogue = []
+        state = 'prologue'
+        try:
+            for line in open(filename):
+                if state == 'prologue':
+                    prologue.append(line)
+                    if line == marker_begin:
+                        state = 'generated'
+                elif state == 'generated':
+                    if line == marker_end:
+                        state = 'epilogue'
+                        epilogue.append(line)
+                elif state == 'epilogue':
+                    epilogue.append(line)
+        except IOError:
+            prologue = [marker_begin]
+            epilogue = [marker_end]
+            state = 'epilogue'
+        if state != 'epilogue':
+            raise Exception('File exists, but generated markers are corrupt '
+                            'or missing')
+
+        outfile = open(filename, 'w')
+        print(''.join(prologue), file=outfile)
+        wrapper = GLWrapper(self.header)
+        if self.system_header:
+            wrapper.preprocessor_parser.system_headers[self.system_header] = \
+                source
+        header_name = self.system_header or self.header
+        wrapper.begin_output(outfile, 
+                             library=None,
+                             link_modules=self.link_modules,
+                             emit_filenames=(header_name,))
+        wrapper.requires_prefix = self.requires_prefix
+        source = self.prologue + source
+        wrapper.wrap(header_name, source)
+        wrapper.end_output()
+        print(''.join(epilogue), file=outfile)
+
+modules = {
+    'gl':  
+        ModuleWrapper(GL_H, 'gl.py'),
+    'glu': 
+        ModuleWrapper(GLU_H, 'glu.py'),
+    'glext_arb': 
+        ModuleWrapper(GLEXT_ABI_H, 'glext_arb.py', 
+            requires_prefix='GL_', system_header='GL/glext.h',
+            prologue='#define GL_GLEXT_PROTOTYPES\n#include <GL/gl.h>\n'),
+    'glext_nv': 
+        ModuleWrapper(GLEXT_NV_H, 'glext_nv.py',
+            requires_prefix='GL_', system_header='GL/glext.h',
+            prologue='#define GL_GLEXT_PROTOTYPES\n#include <GL/gl.h>\n'),
+    'glx': 
+        ModuleWrapper(GLX_H, 'glx.py', 
+            requires_prefix='GLX_',
+            link_modules=('pyglet.libs.x11.xlib',)),
+    'glxext_arb': 
+        ModuleWrapper(GLXEXT_ABI_H, 'glxext_arb.py', requires_prefix='GLX_',
+            system_header='GL/glxext.h',
+            prologue='#define GLX_GLXEXT_PROTOTYPES\n#include <GL/glx.h>\n',
+            link_modules=('pyglet.libs.x11.xlib', 'pyglet.gl.glx')),
+    'glxext_nv': 
+        ModuleWrapper(GLXEXT_NV_H, 'glxext_nv.py', requires_prefix='GLX_',
+            system_header='GL/glxext.h',
+            prologue='#define GLX_GLXEXT_PROTOTYPES\n#include <GL/glx.h>\n',
+            link_modules=('pyglet.libs.x11.xlib', 'pyglet.gl.glx')),
+    'agl':
+        ModuleWrapper(AGL_H, 'agl.py'),
+    'wgl':
+        ModuleWrapper(WGL_H, 'wgl.py'),
+    'wglext_arb':
+        ModuleWrapper(WGLEXT_ABI_H, 'wglext_arb.py', requires_prefix='WGL_',
+            prologue='#define WGL_WGLEXT_PROTOTYPES\n'\
+                     '#include "%s"\n' % WGL_H.encode('string_escape')),
+    'wglext_nv':
+        ModuleWrapper(WGLEXT_NV_H, 'wglext_nv.py', requires_prefix='WGL_',
+            prologue='#define WGL_WGLEXT_PROTOTYPES\n'\
+                     '#include "%s"\n' % WGL_H.encode('string_escape')),
+}
+
+
+if __name__ == '__main__':
+    op = optparse.OptionParser()
+    op.add_option('-D', '--dir', dest='dir',
+                  help='output directory')
+    op.add_option('-r', '--refresh-cache', dest='refresh_cache',
+                  help='clear cache first', action='store_true')
+    options, args = op.parse_args()
+
+    if not options.refresh_cache:
+        load_cache()
+    else:
+        save_cache()
+
+    if not args:
+        print('Specify module(s) to generate:', file=sys.stderr)
+        print('  %s' % ' '.join(modules.keys()), file=sys.stderr)
+
+    if not options.dir:
+        options.dir = os.path.join(script_dir, os.path.pardir, 'pyglet', 'gl')
+    if not os.path.exists(options.dir):
+        os.makedirs(options.dir)
+
+    for arg in args:
+        if arg not in modules:
+            print("Don't know how to make '%s'" % arg, file=sys.stderr)
+            continue
+
+        modules[arg].wrap(options.dir)
+
diff --git a/tools/genwrappers.py b/tools/genwrappers.py
new file mode 100644
index 0000000..53f70ec
--- /dev/null
+++ b/tools/genwrappers.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+
+'''
+'''
+from __future__ import print_function
+
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+from wraptypes.wrap import main as wrap
+import os.path
+import sys
+
+import pyglet
+pyglet.options['shadow_window'] = False
+
+if __name__ == '__main__':
+    if not os.path.exists('pyglet/window'):
+        assert False, 'Run with CWD = trunk root.'
+    names = sys.argv[1:]
+    if pyglet.compat_platform.startswith('linux'):
+        if 'xlib' in names:    
+            wrap('tools/wraptypes/wrap.py',
+                 '-opyglet/libs/x11/xlib.py',
+                 '-lX11',
+                 '/usr/include/X11/Xlib.h',
+                 '/usr/include/X11/X.h',
+                 '/usr/include/X11/Xutil.h')
+        if 'xinerama' in names:
+            wrap('tools/wraptypes/wrap.py',
+                 '-opyglet/libs/x11/xinerama.py',
+                 '-lXinerama',
+                 '-mpyglet.libs.x11.xlib',
+                 '/usr/include/X11/extensions/Xinerama.h')
+        if 'xsync' in names:
+            print('------------------------------------')
+            print('WARNING xsync requires import hacks.')
+            print(' ... copy over from current xsync.py')
+            print('------------------------------------')
+            wrap('tools/wraptypes/wrap.py',
+                 '-opyglet/libs/x11/xsync.py',
+                 '-lXext',
+                 '-mpyglet.libs.x11.xlib',
+                 '-i/usr/include/X11/Xlib.h',
+                 '-i/usr/include/X11/X.h',
+                 '-i/usr/include/X11/Xdefs.h',
+                 '-DStatus=int',
+                 '/usr/include/X11/extensions/sync.h')
+        if 'xinput' in names:
+            wrap('tools/wraptypes/wrap.py',
+                 '-opyglet/libs/x11/xinput.py',
+                 '-lXi',
+                 '-mpyglet.libs.x11.xlib',
+                 '-i/usr/include/X11/Xlib.h',
+                 '-i/usr/include/X11/X.h',
+                 '-i/usr/include/X11/Xdefs.h',
+                 '/usr/include/X11/extensions/XInput.h',
+                 '/usr/include/X11/extensions/XI.h')
+        if 'xrandr' in names:
+            wrap('tools/wraptypes/wrap.py',
+                 '-oexperimental/modeswitch/lib_xrandr.py',
+                 '-lXrandr',
+                 '-mpyglet.libs.x11.xlib',
+                 '-i/usr/include/X11/Xlib.h',
+                 '-i/usr/include/X11/X.h',
+                 '-i/usr/include/X11/Xdefs.h',
+                 '/usr/include/X11/extensions/Xrandr.h',
+                 '/usr/include/X11/extensions/randr.h')
+        if 'xf86vmode' in names:
+            wrap('tools/wraptypes/wrap.py',
+                 '-opyglet/libs/x11/xf86vmode.py',
+                 '-lXxf86vm',
+                 '-mpyglet.libs.x11.xlib',
+                 '-i/usr/include/X11/Xlib.h',
+                 '-i/usr/include/X11/X.h',
+                 '-i/usr/include/X11/Xdefs.h',
+                 '/usr/include/X11/extensions/xf86vmode.h')
+        if 'pulseaudio' in names:
+            wrap('tools/wraptypes/wrap.py',
+                 '-opyglet/media/drivers/pulse/lib_pulseaudio.py',
+                 '-lpulse',
+                 '-i/usr/include/pulse/pulseaudio.h',
+                 '/usr/include/pulse/mainloop-api.h',
+                 '/usr/include/pulse/sample.h',
+                 '/usr/include/pulse/def.h',
+                 '/usr/include/pulse/context.h',
+                 '/usr/include/pulse/stream.h',
+                 '/usr/include/pulse/introspect.h',
+                 '/usr/include/pulse/subscribe.h',
+                 '/usr/include/pulse/scache.h',
+                 '/usr/include/pulse/version.h',
+                 '/usr/include/pulse/error.h',
+                 '/usr/include/pulse/operation.h',
+                 '/usr/include/pulse/channelmap.h',
+                 '/usr/include/pulse/volume.h',
+                 '/usr/include/pulse/xmalloc.h',
+                 '/usr/include/pulse/utf8.h',
+                 '/usr/include/pulse/thread-mainloop.h',
+                 '/usr/include/pulse/mainloop.h',
+                 '/usr/include/pulse/mainloop-signal.h',
+                 '/usr/include/pulse/util.h',
+                 '/usr/include/pulse/timeval.h')
diff --git a/tools/gl_info.py b/tools/gl_info.py
new file mode 100644
index 0000000..72f41dd
--- /dev/null
+++ b/tools/gl_info.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python
+# ----------------------------------------------------------------------------
+# pyglet
+# Copyright (c) 2006-2008 Alex Holkner
+# All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions 
+# are met:
+#
+#  * Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+#  * Redistributions in binary form must reproduce the above copyright 
+#    notice, this list of conditions and the following disclaimer in
+#    the documentation and/or other materials provided with the
+#    distribution.
+#  * Neither the name of pyglet nor the names of its
+#    contributors may be used to endorse or promote products
+#    derived from this software without specific prior written
+#    permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# ----------------------------------------------------------------------------
+
+'''
+'''
+
+__docformat__ = 'restructuredtext'
+__version__ = '1.2'
+
+import sys
+import textwrap
+
+import pyglet
+import pyglet.app
+import pyglet.canvas
+import pyglet.window
+from pyglet.gl import *
+from pyglet.gl import gl_info
+from pyglet.gl import glu_info
+
+print('Pyglet:     %s' % pyglet.version)
+print('Platform:   %s' % sys.platform)
+print('Event loop: %s' % pyglet.app.PlatformEventLoop.__name__)
+
+display = pyglet.canvas.get_display()
+print('Display:    %s' % display.__class__.__name__)
+print('Screens:')
+for screen in display.get_screens():
+    print('  %r' % screen)
+
+print()
+print('Creating default context...')
+w = pyglet.window.Window(1, 1, visible=True)
+print('Window:')
+print('  %s' % w)
+
+print('GL attributes:')
+attrs = w.config.get_gl_attributes()
+attrs = ' '.join(['%s=%s'%(name, value) for name, value in attrs])
+print(' ', '\n  '.join(textwrap.wrap(attrs)))
+
+print('GL version:', gl_info.get_version())
+print('GL vendor:', gl_info.get_vendor())
+print('GL renderer:', gl_info.get_renderer())
+print('GL extensions:')
+exts = ' '.join(gl_info.get_extensions())
+print(' ', '\n  '.join(textwrap.wrap(exts)))
+
+print('GLU version:', glu_info.get_version())
+print('GLU extensions:')
+exts = ' '.join(glu_info.get_extensions())
+print(' ', '\n  '.join(textwrap.wrap(exts)))
+
+print()
+
+context = w.context
+print('Context is', context)
+
+if "xlib" in globals() and isinstance(context, xlib.BaseXlibContext):
+    from pyglet.gl import glx_info
+    print('GLX %s direct'%(context.is_direct() and 'is' or 'is not'))
+    if not glx_info.have_version(1, 1):
+        print("GLX server version: 1.0")
+    else:
+        print('GLX server vendor:', glx_info.get_server_vendor())
+        print('GLX server version:', glx_info.get_server_version())
+        print('GLX server extensions:')
+        exts = glx_info.get_server_extensions()
+        print(' ', '\n  '.join(textwrap.wrap(' '.join(exts))))
+        print('GLX client vendor:', glx_info.get_client_vendor())
+        print('GLX client version:', glx_info.get_client_version())
+        print('GLX client extensions:')
+        exts = glx_info.get_client_extensions()
+        print(' ', '\n  '.join(textwrap.wrap(' '.join(exts))))
+        print('GLX extensions:')
+        exts = glx_info.get_extensions()
+        print(' ', '\n  '.join(textwrap.wrap(' '.join(exts))))
+elif "win32" in globals() and isinstance(context, win32.Win32Context):
+    from pyglet.gl import wgl_info
+    if wgl_info.have_extension('WGL_EXT_extensions_string'):
+        wgl_extensions = wgl_info.get_extensions()
+        print('WGL extensions:')
+        print('', '\n '.join(textwrap.wrap(' '.join(wgl_extensions))))
+    else:
+        print('WGL_EXT_extensions_string extension not available.')
+
+w.close()
diff --git a/tools/license.py b/tools/license.py
new file mode 100755
index 0000000..49f9b82
--- /dev/null
+++ b/tools/license.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+
+import os
+import sys
+import datetime
+import optparse
+"""Rewrite the license header of source files.
+
+Usage:
+    license.py file.py file.py dir/ dir/ ...
+    license.py --help  for more information
+"""
+
+
+license_str = """# pyglet
+# Copyright (c) 2006-2008 Alex Holkner
+# Copyright (c) 2008-{0} pyglet contributors
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+#  * Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+#  * Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in
+#    the documentation and/or other materials provided with the
+#    distribution.
+#  * Neither the name of pyglet nor the names of its
+#    contributors may be used to endorse or promote products
+#    derived from this software without specific prior written
+#    permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.""".format(datetime.datetime.now().year)
+
+marker = '# ' + '-' * 76
+license_lines = [marker] + license_str.split('\n') + [marker]
+skipped_files = 0
+
+
+def update_license(file_name):
+    """Open a Python source file and update the license header in place."""
+    lines = [l.strip('\r\n') for l in open(file_name).readlines()]
+
+    if marker not in lines and options.update_only is True:
+        global skipped_files
+        skipped_files += 1
+
+    elif marker in lines:
+        # Update existing license
+        print("Updating license in: '{0}'".format(file_name))
+        try:
+            marker1 = lines.index(marker)
+            marker2 = lines.index(marker, marker1 + 1)
+            if marker in lines[marker2 + 1:]:
+                raise ValueError()  # too many markers
+            lines = (lines[:marker1] +
+                     license_lines +
+                     lines[marker2 + 1:])
+        except ValueError:
+            print("Can't update license in %s" % file_name, file=sys.stderr)
+
+        open(file_name, 'w').write('\n'.join(lines) + '\n')
+
+    else:
+        # Add license to unmarked file. Skip over #! if present.
+        print("Adding license to: '{0}'".format(file_name))
+        if not lines:
+            pass    # Skip empty files
+        elif lines[0].startswith('#!'):
+            lines = lines[:1] + license_lines + lines[1:]
+        else:
+            lines = license_lines + lines
+
+        open(file_name, 'w').write('\n'.join(lines) + '\n')
+
+
+if __name__ == '__main__':
+    op = optparse.OptionParser(usage=__doc__)
+
+    op.add_option('--exclude', action='append', default=[],
+                  help='specify files and/or folders to exclude')
+    op.add_option('--update-only', action='store_true', default=False,
+                  help='skip files that do not already contain a license header')
+    options, args = op.parse_args()
+
+    if len(args) < 1:
+        print(__doc__, file=sys.stderr)
+        sys.exit(0)
+
+    for path in args:
+        if os.path.isdir(path):
+            for root, dirnames, filenames in os.walk(path):
+                for dirname in dirnames:
+                    if dirname in options.exclude:
+                        dirnames.remove(dirname)
+                for filename in filenames:
+                    if filename.endswith('.py') and filename not in options.exclude:
+                        update_license(os.path.join(root, filename))
+        else:
+            update_license(path)
+
+    if skipped_files > 0:
+        print("Skipped {} files.".format(skipped_files))
diff --git a/tools/wgl.h b/tools/wgl.h
new file mode 100755
index 0000000..93d95cf
--- /dev/null
+++ b/tools/wgl.h
@@ -0,0 +1,201 @@
+/* These are function prototypes and constants needed for WGL.  Derived
+ * from WinGDI.h and gl/GL.h in Visual Studio .NET (7).
+ *  
+ * $Id: $
+ */
+
+#ifndef WGL_H
+#define WGL_H
+
+#define WINAPI
+#define WINGDIAPI
+#define FAR
+#define NEAR
+#define CONST const
+
+
+typedef unsigned int GLenum;
+typedef unsigned char GLboolean;
+typedef unsigned int GLbitfield;
+typedef signed char GLbyte;
+typedef short GLshort;
+typedef int GLint;
+typedef int GLsizei;
+typedef unsigned char GLubyte;
+typedef unsigned short GLushort;
+typedef unsigned int GLuint;
+typedef float GLfloat;
+typedef float GLclampf;
+typedef double GLdouble;
+typedef double GLclampd;
+typedef void GLvoid;
+
+typedef signed char         INT8, *PINT8;
+typedef signed short        INT16, *PINT16;
+typedef signed int          INT32, *PINT32;
+typedef unsigned char       UINT8, *PUINT8;
+typedef unsigned short      UINT16, *PUINT16;
+typedef unsigned int        UINT32, *PUINT32;
+typedef signed int LONG32, *PLONG32;
+typedef unsigned int ULONG32, *PULONG32;
+typedef unsigned int DWORD32, *PDWORD32;
+typedef long long int       INT64, *PINT64;
+typedef unsigned long long int UINT64, *PUINT64;
+
+typedef void VOID, *LPVOID;
+typedef const char *LPCSTR;
+typedef char CHAR;
+typedef unsigned char BYTE;
+typedef unsigned short WORD, USHORT;
+typedef unsigned int UINT;
+typedef int INT, *INT_PTR;
+typedef long BOOL;
+typedef long LONG;
+typedef unsigned long DWORD;
+typedef float FLOAT;
+typedef DWORD COLORREF, *LPCOLORREF;
+
+typedef void *HANDLE;
+#define DECLARE_HANDLE(name) typedef HANDLE name
+DECLARE_HANDLE(HGLRC);
+DECLARE_HANDLE(HDC);
+
+typedef INT_PTR (WINAPI *PROC)();
+
+WINGDIAPI BOOL  WINAPI wglCopyContext(HGLRC, HGLRC, UINT);
+WINGDIAPI HGLRC WINAPI wglCreateContext(HDC);
+WINGDIAPI HGLRC WINAPI wglCreateLayerContext(HDC, int);
+WINGDIAPI BOOL  WINAPI wglDeleteContext(HGLRC);
+WINGDIAPI HGLRC WINAPI wglGetCurrentContext();
+WINGDIAPI HDC   WINAPI wglGetCurrentDC();
+WINGDIAPI PROC  WINAPI wglGetProcAddress(LPCSTR);
+WINGDIAPI BOOL  WINAPI wglMakeCurrent(HDC, HGLRC);
+WINGDIAPI BOOL  WINAPI wglShareLists(HGLRC, HGLRC);
+WINGDIAPI BOOL  WINAPI wglUseFontBitmapsA(HDC, DWORD, DWORD, DWORD);
+WINGDIAPI BOOL  WINAPI wglUseFontBitmapsW(HDC, DWORD, DWORD, DWORD);
+WINGDIAPI BOOL  WINAPI SwapBuffers(HDC);
+
+typedef struct _POINTFLOAT {
+    FLOAT   x;
+    FLOAT   y;
+} POINTFLOAT, *PPOINTFLOAT;
+
+typedef struct _GLYPHMETRICSFLOAT {
+    FLOAT       gmfBlackBoxX;
+    FLOAT       gmfBlackBoxY;
+    POINTFLOAT  gmfptGlyphOrigin;
+    FLOAT       gmfCellIncX;
+    FLOAT       gmfCellIncY;
+} GLYPHMETRICSFLOAT, *PGLYPHMETRICSFLOAT, FAR *LPGLYPHMETRICSFLOAT;
+
+#define WGL_FONT_LINES      0
+#define WGL_FONT_POLYGONS   1
+WINGDIAPI BOOL  WINAPI wglUseFontOutlinesA(HDC, DWORD, DWORD, DWORD, FLOAT,
+                                           FLOAT, int, LPGLYPHMETRICSFLOAT);
+WINGDIAPI BOOL  WINAPI wglUseFontOutlinesW(HDC, DWORD, DWORD, DWORD, FLOAT,
+                                           FLOAT, int, LPGLYPHMETRICSFLOAT);
+
+/* Layer plane descriptor */
+typedef struct tagLAYERPLANEDESCRIPTOR { // lpd
+    WORD  nSize;
+    WORD  nVersion;
+    DWORD dwFlags;
+    BYTE  iPixelType;
+    BYTE  cColorBits;
+    BYTE  cRedBits;
+    BYTE  cRedShift;
+    BYTE  cGreenBits;
+    BYTE  cGreenShift;
+    BYTE  cBlueBits;
+    BYTE  cBlueShift;
+    BYTE  cAlphaBits;
+    BYTE  cAlphaShift;
+    BYTE  cAccumBits;
+    BYTE  cAccumRedBits;
+    BYTE  cAccumGreenBits;
+    BYTE  cAccumBlueBits;
+    BYTE  cAccumAlphaBits;
+    BYTE  cDepthBits;
+    BYTE  cStencilBits;
+    BYTE  cAuxBuffers;
+    BYTE  iLayerPlane;
+    BYTE  bReserved;
+    COLORREF crTransparent;
+} LAYERPLANEDESCRIPTOR, *PLAYERPLANEDESCRIPTOR, FAR *LPLAYERPLANEDESCRIPTOR;
+
+/* LAYERPLANEDESCRIPTOR flags */
+#define LPD_DOUBLEBUFFER        0x00000001
+#define LPD_STEREO              0x00000002
+#define LPD_SUPPORT_GDI         0x00000010
+#define LPD_SUPPORT_OPENGL      0x00000020
+#define LPD_SHARE_DEPTH         0x00000040
+#define LPD_SHARE_STENCIL       0x00000080
+#define LPD_SHARE_ACCUM         0x00000100
+#define LPD_SWAP_EXCHANGE       0x00000200
+#define LPD_SWAP_COPY           0x00000400
+#define LPD_TRANSPARENT         0x00001000
+
+#define LPD_TYPE_RGBA        0
+#define LPD_TYPE_COLORINDEX  1
+
+/* wglSwapLayerBuffers flags */
+#define WGL_SWAP_MAIN_PLANE     0x00000001
+#define WGL_SWAP_OVERLAY1       0x00000002
+#define WGL_SWAP_OVERLAY2       0x00000004
+#define WGL_SWAP_OVERLAY3       0x00000008
+#define WGL_SWAP_OVERLAY4       0x00000010
+#define WGL_SWAP_OVERLAY5       0x00000020
+#define WGL_SWAP_OVERLAY6       0x00000040
+#define WGL_SWAP_OVERLAY7       0x00000080
+#define WGL_SWAP_OVERLAY8       0x00000100
+#define WGL_SWAP_OVERLAY9       0x00000200
+#define WGL_SWAP_OVERLAY10      0x00000400
+#define WGL_SWAP_OVERLAY11      0x00000800
+#define WGL_SWAP_OVERLAY12      0x00001000
+#define WGL_SWAP_OVERLAY13      0x00002000
+#define WGL_SWAP_OVERLAY14      0x00004000
+#define WGL_SWAP_OVERLAY15      0x00008000
+#define WGL_SWAP_UNDERLAY1      0x00010000
+#define WGL_SWAP_UNDERLAY2      0x00020000
+#define WGL_SWAP_UNDERLAY3      0x00040000
+#define WGL_SWAP_UNDERLAY4      0x00080000
+#define WGL_SWAP_UNDERLAY5      0x00100000
+#define WGL_SWAP_UNDERLAY6      0x00200000
+#define WGL_SWAP_UNDERLAY7      0x00400000
+#define WGL_SWAP_UNDERLAY8      0x00800000
+#define WGL_SWAP_UNDERLAY9      0x01000000
+#define WGL_SWAP_UNDERLAY10     0x02000000
+#define WGL_SWAP_UNDERLAY11     0x04000000
+#define WGL_SWAP_UNDERLAY12     0x08000000
+#define WGL_SWAP_UNDERLAY13     0x10000000
+#define WGL_SWAP_UNDERLAY14     0x20000000
+#define WGL_SWAP_UNDERLAY15     0x40000000
+
+WINGDIAPI BOOL  WINAPI wglDescribeLayerPlane(HDC, int, int, UINT,
+                                             LPLAYERPLANEDESCRIPTOR);
+WINGDIAPI int   WINAPI wglSetLayerPaletteEntries(HDC, int, int, int,
+                                                 CONST COLORREF *);
+WINGDIAPI int   WINAPI wglGetLayerPaletteEntries(HDC, int, int, int,
+                                                 COLORREF *);
+WINGDIAPI BOOL  WINAPI wglRealizeLayerPalette(HDC, int, BOOL);
+WINGDIAPI BOOL  WINAPI wglSwapLayerBuffers(HDC, UINT);
+
+typedef struct _WGLSWAP
+{
+    HDC hdc;
+    UINT uiFlags;
+} WGLSWAP, *PWGLSWAP, FAR *LPWGLSWAP;
+
+#define WGL_SWAPMULTIPLE_MAX 16
+
+WINGDIAPI DWORD WINAPI wglSwapMultipleBuffers(UINT, CONST WGLSWAP *);
+
+typedef struct tagRECT
+{
+    LONG    left;
+    LONG    top;
+    LONG    right;
+    LONG    bottom;
+} RECT, *PRECT, NEAR *NPRECT, FAR *LPRECT;
+
+#endif /* WGL_H */
diff --git a/tools/wraptypes/__init__.py b/tools/wraptypes/__init__.py
new file mode 100644
index 0000000..509a769
--- /dev/null
+++ b/tools/wraptypes/__init__.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+
+'''A package for generating ctypes wrappers from a C header file.  See the
+docstring for wrap.py for usage.
+
+The files 'lex.py' and 'yacc.py' are from PLY (http://www.dabeaz.com/ply),
+which was written by David M. Beazley but have been modified slightly
+for this tool.
+'''
+
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+
diff --git a/tools/wraptypes/cparser.py b/tools/wraptypes/cparser.py
new file mode 100644
index 0000000..16e8c68
--- /dev/null
+++ b/tools/wraptypes/cparser.py
@@ -0,0 +1,1452 @@
+#!/usr/bin/env python
+
+'''Parse a C source file.
+
+To use, subclass CParser and override its handle_* methods.  Then instantiate
+the class with a string to parse.
+
+Derived from ANSI C grammar:
+  * Lexicon: http://www.lysator.liu.se/c/ANSI-C-grammar-l.html
+  * Grammar: http://www.lysator.liu.se/c/ANSI-C-grammar-y.html
+
+Reference is C99:
+  * http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1124.pdf
+
+'''
+from __future__ import print_function
+
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+import cPickle
+import operator
+import os.path
+import re
+import sys
+import time
+import warnings
+
+import preprocessor
+import yacc
+
+tokens = (
+
+    'PP_IF', 'PP_IFDEF', 'PP_IFNDEF', 'PP_ELIF', 'PP_ELSE',
+    'PP_ENDIF', 'PP_INCLUDE', 'PP_DEFINE', 'PP_DEFINE_CONSTANT', 'PP_UNDEF',
+    'PP_LINE', 'PP_ERROR', 'PP_PRAGMA',
+
+    'IDENTIFIER', 'CONSTANT', 'CHARACTER_CONSTANT', 'STRING_LITERAL', 'SIZEOF',
+    'PTR_OP', 'INC_OP', 'DEC_OP', 'LEFT_OP', 'RIGHT_OP', 'LE_OP', 'GE_OP',
+    'EQ_OP', 'NE_OP', 'AND_OP', 'OR_OP', 'MUL_ASSIGN', 'DIV_ASSIGN',
+    'MOD_ASSIGN', 'ADD_ASSIGN', 'SUB_ASSIGN', 'LEFT_ASSIGN', 'RIGHT_ASSIGN',
+    'AND_ASSIGN', 'XOR_ASSIGN', 'OR_ASSIGN',  'HASH_HASH', 'PERIOD',
+    'TYPE_NAME', 
+    
+    'TYPEDEF', 'EXTERN', 'STATIC', 'AUTO', 'REGISTER', 
+    'CHAR', 'SHORT', 'INT', 'LONG', 'SIGNED', 'UNSIGNED', 'FLOAT', 'DOUBLE',
+    'CONST', 'VOLATILE', 'VOID',
+    'STRUCT', 'UNION', 'ENUM', 'ELLIPSIS',
+
+    'CASE', 'DEFAULT', 'IF', 'ELSE', 'SWITCH', 'WHILE', 'DO', 'FOR', 'GOTO',
+    'CONTINUE', 'BREAK', 'RETURN', '__ASM__'
+)
+
+keywords = [
+    'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do',
+    'double', 'else', 'enum', 'extern', 'float', 'for', 'goto', 'if', 'int',
+    'long', 'register', 'return', 'short', 'signed', 'sizeof', 'static',
+    'struct', 'switch', 'typedef', 'union', 'unsigned', 'void', 'volatile',
+    'while', '__asm__'
+]
+
+
+# --------------------------------------------------------------------------
+# C Object Model
+# --------------------------------------------------------------------------
+
+class Declaration(object):
+    def __init__(self):
+        self.declarator = None
+        self.type = Type()
+        self.storage = None
+
+    def __repr__(self):
+        d = {
+            'declarator': self.declarator,
+            'type': self.type,
+        }
+        if self.storage:
+            d['storage'] = self.storage
+        l = ['%s=%r' % (k, v) for k, v in d.items()]
+        return 'Declaration(%s)' % ', '.join(l)
+
+class Declarator(object):
+    pointer = None
+    def __init__(self):
+        self.identifier = None
+        self.initializer = None
+        self.array = None
+        self.parameters = None
+
+    # make pointer read-only to catch mistakes early
+    pointer = property(lambda self: None)
+
+    def __repr__(self):
+        s = self.identifier or ''
+        if self.array:
+            s += repr(self.array)
+        if self.initializer:
+            s += ' = %r' % self.initializer
+        if self.parameters is not None:
+            s += '(' + ', '.join([repr(p) for p in self.parameters]) + ')'
+        return s
+
+class Pointer(Declarator):
+    pointer = None
+    def __init__(self):
+        super(Pointer, self).__init__()
+        self.qualifiers = []
+
+    def __repr__(self):
+        q = ''
+        if self.qualifiers:
+            q = '<%s>' % ' '.join(self.qualifiers)
+        return 'POINTER%s(%r)' % (q, self.pointer) + \
+            super(Pointer, self).__repr__()
+
+class Array(object):
+    def __init__(self):
+        self.size = None
+        self.array = None
+
+    def __repr__(self):
+        if self.size:
+            a =  '[%r]' % self.size
+        else:
+            a = '[]'
+        if self.array:
+            return repr(self.array) + a
+        else:
+            return a
+
+class Parameter(object):
+    def __init__(self):
+        self.type = Type()
+        self.storage = None
+        self.declarator = None
+
+    def __repr__(self):
+        d = {
+            'type': self.type,
+        }
+        if self.declarator:
+            d['declarator'] = self.declarator
+        if self.storage:
+            d['storage'] = self.storage
+        l = ['%s=%r' % (k, v) for k, v in d.items()]
+        return 'Parameter(%s)' % ', '.join(l)
+
+
+class Type(object):
+    def __init__(self):
+        self.qualifiers = []
+        self.specifiers = []
+
+    def __repr__(self):
+        return ' '.join(self.qualifiers + [str(s) for s in self.specifiers])
+
+# These are used only internally.
+
+class StorageClassSpecifier(str):
+    pass
+
+class TypeSpecifier(str):
+    pass
+
+class StructTypeSpecifier(object):
+    def __init__(self, is_union, tag, declarations):
+        self.is_union = is_union
+        self.tag = tag
+        self.declarations = declarations
+
+    def __repr__(self):
+        if self.is_union:
+            s = 'union'
+        else:
+            s = 'struct'
+        if self.tag:
+            s += ' %s' % self.tag
+        if self.declarations:
+            s += ' {%s}' % '; '.join([repr(d) for d in self.declarations])
+        return s
+
+class EnumSpecifier(object):
+    def __init__(self, tag, enumerators):
+        self.tag = tag
+        self.enumerators = enumerators
+
+    def __repr__(self):
+        s = 'enum'
+        if self.tag:
+            s += ' %s' % self.tag
+        if self.enumerators:
+            s += ' {%s}' % ', '.join([repr(e) for e in self.enumerators])
+        return s
+
+class Enumerator(object):
+    def __init__(self, name, expression):
+        self.name = name
+        self.expression = expression
+
+    def __repr__(self):
+        s = self.name
+        if self.expression:
+            s += ' = %r' % self.expression
+        return s
+
+class TypeQualifier(str):
+    pass
+
+def apply_specifiers(specifiers, declaration):
+    '''Apply specifiers to the declaration (declaration may be
+    a Parameter instead).'''
+    for s in specifiers:
+        if type(s) == StorageClassSpecifier:
+            if declaration.storage:
+                p.parser.cparser.handle_error(
+                    'Declaration has more than one storage class', 
+                    '???', p.lineno(1))
+                return
+            declaration.storage = s
+        elif type(s) in (TypeSpecifier, StructTypeSpecifier, EnumSpecifier):
+            declaration.type.specifiers.append(s)
+        elif type(s) == TypeQualifier:
+            declaration.type.qualifiers.append(s)
+
+
+# --------------------------------------------------------------------------
+# Expression Object Model
+# --------------------------------------------------------------------------
+
+class EvaluationContext(object):
+    '''Interface for evaluating expression nodes.
+    '''
+    def evaluate_identifier(self, name):
+        warnings.warn('Attempt to evaluate identifier "%s" failed' % name)
+        return 0
+
+    def evaluate_sizeof(self, type):
+        warnings.warn('Attempt to evaluate sizeof "%s" failed' % str(type))
+        return 0
+
+class ExpressionNode(object):
+    def evaluate(self, context):
+        return 0
+
+    def __str__(self):
+        return ''
+
+class ConstantExpressionNode(ExpressionNode):
+    def __init__(self, value):
+        self.value = value
+
+    def evaluate(self, context):
+        return self.value
+
+    def __str__(self):
+        return str(self.value)
+
+class IdentifierExpressionNode(ExpressionNode):
+    def __init__(self, name):
+        self.name = name
+
+    def evaluate(self, context):
+        return context.evaluate_identifier(self.name)
+
+    def __str__(self):
+        return str(self.value)
+
+class UnaryExpressionNode(ExpressionNode):
+    def __init__(self, op, op_str, child):
+        self.op = op
+        self.op_str = op_str
+        self.child = child
+
+    def evaluate(self, context):
+        return self.op(self.child.evaluate(context))
+
+    def __str__(self):
+        return '(%s %s)' % (self.op_str, self.child)
+
+class SizeOfExpressionNode(ExpressionNode):
+    def __init__(self, type):
+        self.type = type
+
+    def evaluate(self, context):
+        return context.evaluate_sizeof(self.type)
+
+    def __str__(self):
+        return 'sizeof(%s)' % str(self.type)
+
+class BinaryExpressionNode(ExpressionNode):
+    def __init__(self, op, op_str, left, right):
+        self.op = op
+        self.op_str = op_str
+        self.left = left
+        self.right = right
+
+    def evaluate(self, context):
+        return self.op(self.left.evaluate(context), 
+                       self.right.evaluate(context))
+
+    def __str__(self):
+        return '(%s %s %s)' % (self.left, self.op_str, self.right)
+
+class LogicalAndExpressionNode(ExpressionNode):
+    def __init__(self, left, right):
+        self.left = left
+        self.right = right
+
+    def evaluate(self, context):
+        return self.left.evaluate(context) and self.right.evaluate(context)
+
+    def __str__(self):
+        return '(%s && %s)' % (self.left, self.right)
+
+class LogicalOrExpressionNode(ExpressionNode):
+    def __init__(self, left, right):
+        self.left = left
+        self.right = right
+
+    def evaluate(self, context):
+        return self.left.evaluate(context) or self.right.evaluate(context)
+
+    def __str__(self):
+        return '(%s || %s)' % (self.left, self.right)
+
+class ConditionalExpressionNode(ExpressionNode):
+    def __init__(self, condition, left, right):
+        self.condition = condition
+        self.left = left
+        self.right = right
+
+    def evaluate(self, context):
+        if self.condition.evaluate(context):
+            return self.left.evaluate(context)
+        else:
+            return self.right.evaluate(context)
+
+    def __str__(self):
+        return '(%s ? %s : %s)' % (self.condition, self.left, self.right)
+
+
+# --------------------------------------------------------------------------
+# Grammar
+# --------------------------------------------------------------------------
+
+def p_translation_unit(p):
+    '''translation_unit : 
+                        | translation_unit external_declaration
+    '''
+    # Starting production.
+    # Allow empty production so that files with no declarations are still
+    #    valid.
+    # Intentionally empty
+
+def p_identifier(p):
+    '''identifier : IDENTIFIER'''
+    p[0] = IdentifierExpressionNode(p[1])
+
+def p_constant(p):
+    '''constant : CONSTANT
+                | CHARACTER_CONSTANT
+    '''
+    def _to_int(s):
+        s = s.rstrip('lLuU')
+        if s.startswith('0x'):
+            return int(s, base=16)
+        elif s.startswith('0'):
+            return int(s, base=8)
+        else:
+            return int(s)
+
+    value = p[1]
+    try:
+        value = _to_int(value)
+    except ValueError:
+        pass
+    p[0] = ConstantExpressionNode(value)
+
+def p_string_literal(p):
+    '''string_literal : STRING_LITERAL'''
+    p[0] = ConstantExpressionNode(p[1])
+
+def p_primary_expression(p):
+    '''primary_expression : identifier
+                          | constant
+                          | string_literal
+                          | '(' expression ')'
+    '''
+    if p[1] == '(':
+        p[0] = p[2]
+    else:
+        p[0] = p[1]
+
+def p_postfix_expression(p):
+    '''postfix_expression : primary_expression
+                  | postfix_expression '[' expression ']'
+                  | postfix_expression '(' ')'
+                  | postfix_expression '(' argument_expression_list ')'
+                  | postfix_expression PERIOD IDENTIFIER
+                  | postfix_expression PTR_OP IDENTIFIER
+                  | postfix_expression INC_OP
+                  | postfix_expression DEC_OP
+    '''
+    # XXX Largely unsupported
+    p[0] = p[1]
+
+def p_argument_expression_list(p):
+    '''argument_expression_list : assignment_expression
+                        | argument_expression_list ',' assignment_expression
+    '''
+
+def p_asm_expression(p):
+    '''asm_expression : __ASM__ volatile_opt '(' string_literal ')'
+                      | __ASM__ volatile_opt '(' string_literal ':' str_opt_expr_pair_list ')'
+                      | __ASM__ volatile_opt '(' string_literal ':' str_opt_expr_pair_list ':' str_opt_expr_pair_list ')'
+                      | __ASM__ volatile_opt '(' string_literal ':' str_opt_expr_pair_list ':' str_opt_expr_pair_list ':' str_opt_expr_pair_list ')'
+    '''
+
+    # Definitely not ISO C, adapted from example ANTLR GCC parser at
+    #  http://www.antlr.org/grammar/cgram//grammars/GnuCParser.g
+    # but more lenient (expressions permitted in optional final part, when
+    # they shouldn't be -- avoids shift/reduce conflict with
+    # str_opt_expr_pair_list).
+
+    # XXX node not supported
+    p[0] = ExpressionNode()
+
+def p_str_opt_expr_pair_list(p):
+    '''str_opt_expr_pair_list : 
+                              | str_opt_expr_pair
+                              | str_opt_expr_pair_list ',' str_opt_expr_pair
+    '''
+
+def p_str_opt_expr_pair(p):
+   '''str_opt_expr_pair : string_literal
+                        | string_literal '(' expression ')'
+    '''
+
+def p_volatile_opt(p):
+    '''volatile_opt : 
+                    | VOLATILE
+    '''
+
+def p_unary_expression(p):
+    '''unary_expression : postfix_expression
+                        | INC_OP unary_expression
+                        | DEC_OP unary_expression
+                        | unary_operator cast_expression
+                        | SIZEOF unary_expression
+                        | SIZEOF '(' type_name ')'
+                        | asm_expression
+    '''
+    if len(p) == 2:
+        p[0] = p[1]
+    elif p[1] == 'sizeof':
+        if p[2] == '(':
+            p[0] = SizeOfExpressionNode(p[3])
+        else:
+            p[0] = SizeOfExpressionNode(p[2])
+    elif type(p[1]) == tuple:
+        # unary_operator reduces to (op, op_str)
+        p[0] = UnaryExpressionNode(p[1][0], p[1][1], p[2])
+    else:
+        # XXX INC_OP and DEC_OP expression nodes not supported
+        p[0] = p[2]
+
+def p_unary_operator(p):
+    '''unary_operator : '&'
+                      | '*'
+                      | '+'
+                      | '-'
+                      | '~'
+                      | '!'
+    '''
+    # reduces to (op, op_str)
+    p[0] = ({
+        '+': operator.pos,
+        '-': operator.neg,
+        '~': operator.inv,
+        '!': operator.not_,
+        '&': 'AddressOfUnaryOperator',
+        '*': 'DereferenceUnaryOperator'}[p[1]], p[1])
+
+def p_cast_expression(p):
+    '''cast_expression : unary_expression
+                       | '(' type_name ')' cast_expression
+    '''
+    if len(p) == 2:
+        p[0] = p[1]
+    else:
+        # XXX cast node not supported
+        p[0] = p[4]
+
+def p_multiplicative_expression(p):
+    '''multiplicative_expression : cast_expression
+                                 | multiplicative_expression '*' cast_expression
+                                 | multiplicative_expression '/' cast_expression
+                                 | multiplicative_expression '%' cast_expression
+    '''
+    if len(p) == 2:
+        p[0] = p[1]
+    else:
+        p[0] = BinaryExpressionNode({
+            '*': operator.mul,
+            '/': operator.div,
+            '%': operator.mod}[p[2]], p[2], p[1], p[3])
+
+def p_additive_expression(p):
+    '''additive_expression : multiplicative_expression
+                           | additive_expression '+' multiplicative_expression
+                           | additive_expression '-' multiplicative_expression
+    '''
+    if len(p) == 2:
+        p[0] = p[1]
+    else:
+        p[0] = BinaryExpressionNode({
+            '+': operator.add,
+            '-': operator.sub}[p[2]], p[2], p[1], p[3])
+
+def p_shift_expression(p):
+    '''shift_expression : additive_expression
+                        | shift_expression LEFT_OP additive_expression
+                        | shift_expression RIGHT_OP additive_expression
+    '''
+    if len(p) == 2:
+        p[0] = p[1]
+    else:
+        p[0] = BinaryExpressionNode({
+            '<<': operator.lshift,
+            '>>': operator.rshift}[p[2]], p[2], p[1], p[3])
+        
+def p_relational_expression(p):
+    '''relational_expression : shift_expression 
+                             | relational_expression '<' shift_expression
+                             | relational_expression '>' shift_expression
+                             | relational_expression LE_OP shift_expression
+                             | relational_expression GE_OP shift_expression
+    '''
+    if len(p) == 2:
+        p[0] = p[1]
+    else:
+        p[0] = BinaryExpressionNode({
+            '>': operator.gt,
+            '<': operator.lt,
+            '<=': operator.le,
+            '>=': operator.ge}[p[2]], p[2], p[1], p[3])
+
+def p_equality_expression(p):
+    '''equality_expression : relational_expression
+                           | equality_expression EQ_OP relational_expression
+                           | equality_expression NE_OP relational_expression
+    '''
+    if len(p) == 2:
+        p[0] = p[1]
+    else:
+        p[0] = BinaryExpressionNode({
+            '==': operator.eq,
+            '!=': operator.ne}[p[2]], p[2], p[1], p[3])
+
+def p_and_expression(p):
+    '''and_expression : equality_expression
+                      | and_expression '&' equality_expression
+    '''
+    if len(p) == 2:
+        p[0] = p[1]
+    else:
+        p[0] = BinaryExpressionNode(operator.and_, '&', p[1], p[3])
+
+def p_exclusive_or_expression(p):
+    '''exclusive_or_expression : and_expression
+                               | exclusive_or_expression '^' and_expression
+    ''' 
+    if len(p) == 2:
+        p[0] = p[1]
+    else:
+        p[0] = BinaryExpressionNode(operator.xor, '^', p[1], p[3])
+
+def p_inclusive_or_expression(p):
+    '''inclusive_or_expression : exclusive_or_expression
+                   | inclusive_or_expression '|' exclusive_or_expression
+    '''
+    if len(p) == 2:
+        p[0] = p[1]
+    else:
+        p[0] = BinaryExpressionNode(operator.or_, '|', p[1], p[3])
+
+def p_logical_and_expression(p):
+    '''logical_and_expression : inclusive_or_expression
+                  | logical_and_expression AND_OP inclusive_or_expression
+    '''
+    if len(p) == 2:
+        p[0] = p[1]
+    else:
+        p[0] = LogicalAndExpressionNode(p[1], p[3])
+
+def p_logical_or_expression(p):
+    '''logical_or_expression : logical_and_expression
+                  | logical_or_expression OR_OP logical_and_expression
+    '''
+    if len(p) == 2:
+        p[0] = p[1]
+    else:
+        p[0] = LogicalOrExpressionNode(p[1], p[3])
+
+
+def p_conditional_expression(p):
+    '''conditional_expression : logical_or_expression
+          | logical_or_expression '?' expression ':' conditional_expression
+    '''
+    if len(p) == 2:
+        p[0] = p[1]
+    else:
+        p[0] = ConditionalExpressionNode(p[1], p[3], p[5])
+
+def p_assignment_expression(p):
+    '''assignment_expression : conditional_expression
+                 | unary_expression assignment_operator assignment_expression
+    '''
+    if len(p) == 2:
+        p[0] = p[1]
+    else:
+        # XXX assignment expression node not supported
+        p[0] = p[3]
+
+def p_assignment_operator(p):
+    '''assignment_operator : '='
+                           | MUL_ASSIGN
+                           | DIV_ASSIGN
+                           | MOD_ASSIGN
+                           | ADD_ASSIGN
+                           | SUB_ASSIGN
+                           | LEFT_ASSIGN
+                           | RIGHT_ASSIGN
+                           | AND_ASSIGN
+                           | XOR_ASSIGN
+                           | OR_ASSIGN
+    '''
+
+def p_expression(p):
+    '''expression : assignment_expression
+                  | expression ',' assignment_expression
+    '''
+    p[0] = p[1]
+    # XXX sequence expression node not supported 
+
+def p_constant_expression(p):
+    '''constant_expression : conditional_expression
+    '''
+    p[0] = p[1]
+
+def p_declaration(p):
+    '''declaration : declaration_impl ';'
+    '''
+    # The ';' must be here, not in 'declaration', as declaration needs to
+    # be executed before the ';' is shifted (otherwise the next lookahead will
+    # be read, which may be affected by this declaration if its a typedef.
+
+def p_declaration_impl(p):
+    '''declaration_impl : declaration_specifiers
+                        | declaration_specifiers init_declarator_list
+    '''
+    declaration = Declaration()
+    apply_specifiers(p[1], declaration)
+
+    if len(p) == 2:
+        filename = p.slice[1].filename
+        lineno = p.slice[1].lineno
+        p.parser.cparser.impl_handle_declaration(declaration, filename, lineno)
+        return
+
+    filename = p.slice[2].filename
+    lineno = p.slice[2].lineno
+    for declarator in p[2]:
+        declaration.declarator = declarator
+        p.parser.cparser.impl_handle_declaration(declaration, filename, lineno)
+
+""" # shift/reduce conflict with p_statement_error.
+def p_declaration_error(p):
+    '''declaration : error ';'
+    '''
+    # Error resynchronisation catch-all
+"""
+
+def p_declaration_specifiers(p):
+    '''declaration_specifiers : storage_class_specifier
+                              | storage_class_specifier declaration_specifiers
+                              | type_specifier
+                              | type_specifier declaration_specifiers
+                              | type_qualifier
+                              | type_qualifier declaration_specifiers
+    '''
+    if len(p) > 2:
+        p[0] = (p[1],) + p[2]
+    else:
+        p[0] = (p[1],)
+
+def p_init_declarator_list(p):
+    '''init_declarator_list : init_declarator
+                            | init_declarator_list ',' init_declarator
+    '''
+    if len(p) > 2:
+        p[0] = p[1] + (p[3],)
+    else:
+        p[0] = (p[1],)
+
+def p_init_declarator(p):
+    '''init_declarator : declarator
+                       | declarator '=' initializer
+    '''
+    p[0] = p[1]
+    if len(p) > 2:
+        p[0].initializer = p[2]
+
+def p_storage_class_specifier(p):
+    '''storage_class_specifier : TYPEDEF
+                               | EXTERN
+                               | STATIC
+                               | AUTO
+                               | REGISTER
+    '''
+    p[0] = StorageClassSpecifier(p[1])
+
+def p_type_specifier(p):
+    '''type_specifier : VOID
+                      | CHAR
+                      | SHORT
+                      | INT
+                      | LONG
+                      | FLOAT
+                      | DOUBLE
+                      | SIGNED
+                      | UNSIGNED
+                      | struct_or_union_specifier
+                      | enum_specifier
+                      | TYPE_NAME
+    '''
+    if type(p[1]) in (StructTypeSpecifier, EnumSpecifier):
+        p[0] = p[1]
+    else:
+        p[0] = TypeSpecifier(p[1])
+    # TODO enum
+
+def p_struct_or_union_specifier(p):
+    '''struct_or_union_specifier : struct_or_union IDENTIFIER '{' struct_declaration_list '}'
+         | struct_or_union TYPE_NAME '{' struct_declaration_list '}'
+         | struct_or_union '{' struct_declaration_list '}'
+         | struct_or_union IDENTIFIER
+         | struct_or_union TYPE_NAME
+    '''
+    # The TYPE_NAME ones are dodgy, needed for Apple headers
+    # CoreServices.framework/Frameworks/CarbonCore.framework/Headers/Files.h.
+    # CoreServices.framework/Frameworks/OSServices.framework/Headers/Power.h
+    if len(p) == 3:
+        p[0] = StructTypeSpecifier(p[1], p[2], None)
+    elif p[2] == '{':
+        p[0] = StructTypeSpecifier(p[1], '', p[3])
+    else:
+        p[0] = StructTypeSpecifier(p[1], p[2], p[4])
+
+def p_struct_or_union(p):
+    '''struct_or_union : STRUCT
+                       | UNION
+    '''
+    p[0] = p[1] == 'union'
+
+def p_struct_declaration_list(p):
+    '''struct_declaration_list : struct_declaration
+                               | struct_declaration_list struct_declaration
+    '''
+    if len(p) == 2:
+        p[0] = p[1]
+    else:
+        p[0] = p[1] + p[2]
+
+def p_struct_declaration(p):
+    '''struct_declaration : specifier_qualifier_list struct_declarator_list ';'
+    '''
+    # p[0] returned is a tuple, to handle multiple declarators in one
+    # declaration.
+    r = ()
+    for declarator in p[2]:
+        declaration = Declaration()
+        apply_specifiers(p[1], declaration)
+        declaration.declarator = declarator
+        r += (declaration,)
+    p[0] = r
+
+def p_specifier_qualifier_list(p):
+    '''specifier_qualifier_list : type_specifier specifier_qualifier_list
+                                | type_specifier
+                                | type_qualifier specifier_qualifier_list
+                                | type_qualifier
+    '''
+    # XXX Interesting.. why is this one right-recursion?
+    if len(p) == 3:
+        p[0] = (p[1],) + p[2]
+    else:
+        p[0] = (p[1],)
+
+def p_struct_declarator_list(p):
+    '''struct_declarator_list : struct_declarator
+                              | struct_declarator_list ',' struct_declarator
+    '''
+    if len(p) == 2:
+        p[0] = (p[1],)
+    else:
+        p[0] = p[1] + (p[3],)
+
+def p_struct_declarator(p):
+    '''struct_declarator : declarator
+                         | ':' constant_expression
+                         | declarator ':' constant_expression
+    '''
+    # XXX ignoring bitfields.
+    if p[1] == ':':
+        p[0] = Declarator()
+    else:
+        p[0] = p[1]
+
+def p_enum_specifier(p):
+    '''enum_specifier : ENUM '{' enumerator_list '}'
+                      | ENUM IDENTIFIER '{' enumerator_list '}'
+                      | ENUM IDENTIFIER
+    '''
+    if len(p) == 5:
+        p[0] = EnumSpecifier(None, p[3])
+    elif len(p) == 6:
+        p[0] = EnumSpecifier(p[2], p[4])
+    else:
+        p[0] = EnumSpecifier(p[2], ())
+
+def p_enumerator_list(p):
+    '''enumerator_list : enumerator_list_iso
+                       | enumerator_list_iso ','
+    '''
+    # Apple headers sometimes have trailing ',' after enumerants, which is
+    # not ISO C.
+    p[0] = p[1]
+
+def p_enumerator_list_iso(p):
+    '''enumerator_list_iso : enumerator
+                           | enumerator_list_iso ',' enumerator
+    '''
+    if len(p) == 2:
+        p[0] = (p[1],)
+    else:
+        p[0] = p[1] + (p[3],)
+
+def p_enumerator(p):
+    '''enumerator : IDENTIFIER
+                  | IDENTIFIER '=' constant_expression
+    '''
+    if len(p) == 2:
+        p[0] = Enumerator(p[1], None)
+    else:
+        p[0] = Enumerator(p[1], p[3])
+
+def p_type_qualifier(p):
+    '''type_qualifier : CONST
+                      | VOLATILE
+    '''
+    p[0] = TypeQualifier(p[1])
+
+def p_declarator(p):
+    '''declarator : pointer direct_declarator
+                  | direct_declarator
+    '''
+    if len(p) > 2:
+        p[0] = p[1]
+        ptr = p[1]
+        while ptr.pointer:
+            ptr = ptr.pointer
+        ptr.pointer = p[2]
+    else:
+        p[0] = p[1]
+
+def p_direct_declarator(p):
+    '''direct_declarator : IDENTIFIER
+                         | TYPE_NAME
+                         | '(' declarator ')'
+                         | direct_declarator '[' constant_expression ']'
+                         | direct_declarator '[' ']'
+                         | direct_declarator '(' parameter_type_list ')'
+                         | direct_declarator '(' identifier_list ')'
+                         | direct_declarator '(' ')'
+    '''
+    # TYPE_NAME path is because some types are predefined in wrapper scripts;
+    # it is not valid C.
+
+    if isinstance(p[1], Declarator):
+        p[0] = p[1] 
+        if p[2] == '[':
+            a = Array()
+            a.array = p[0].array
+            p[0].array = a
+            if p[3] != ']':
+                a.size = p[3]
+        else:
+            if p[3] == ')':
+                p[0].parameters = ()
+            else:
+                p[0].parameters = p[3]
+    elif p[1] == '(':
+        p[0] = p[2]
+    else:
+        p[0] = Declarator()
+        p[0].identifier = p[1]
+
+    # Check parameters for (void) and simplify to empty tuple.
+    if p[0].parameters and len(p[0].parameters) == 1:
+        param = p[0].parameters[0]
+        if param.type.specifiers == ['void'] and not param.declarator:
+            p[0].parameters = ()
+
+
+def p_pointer(p):
+    '''pointer : '*'
+               | '*' type_qualifier_list
+               | '*' pointer
+               | '*' type_qualifier_list pointer
+    '''
+    if len(p) == 2:
+        p[0] = Pointer()
+    elif len(p) == 3:
+        if type(p[2]) == Pointer:
+            p[0] = Pointer()
+            p[0].pointer = p[2]
+        else:
+            p[0] = Pointer()
+            p[0].qualifiers = p[2]
+    else:
+        p[0] = Pointer()
+        p[0].qualifiers = p[2]
+        p[0].pointer = p[3]
+
+def p_type_qualifier_list(p):
+    '''type_qualifier_list : type_qualifier
+                           | type_qualifier_list type_qualifier
+    '''
+    if len(p) > 2:
+        p[0] = p[1] + (p[2],)
+    else:
+        p[0] = (p[1],)
+
+def p_parameter_type_list(p):
+    '''parameter_type_list : parameter_list
+                           | parameter_list ',' ELLIPSIS
+    '''
+    if len(p) > 2:
+        p[0] = p[1] + (p[3],)
+    else:
+        p[0] = p[1]
+
+
+def p_parameter_list(p):
+    '''parameter_list : parameter_declaration
+                      | parameter_list ',' parameter_declaration
+    '''
+    if len(p) > 2:
+        p[0] = p[1] + (p[3],)
+    else:
+        p[0] = (p[1],)
+
+def p_parameter_declaration(p):
+    '''parameter_declaration : declaration_specifiers declarator
+                             | declaration_specifiers abstract_declarator
+                             | declaration_specifiers
+    '''
+    p[0] = Parameter()
+    apply_specifiers(p[1], p[0])
+    if len(p) > 2:
+        p[0].declarator = p[2]
+
+def p_identifier_list(p):
+    '''identifier_list : IDENTIFIER
+                       | identifier_list ',' IDENTIFIER
+    '''
+    param = Parameter()
+    param.declarator = Declarator()
+    if len(p) > 2:
+        param.declarator.identifier = p[3]
+        p[0] = p[1] + (param,)
+    else:
+        param.declarator.identifier = p[1]
+        p[0] = (param,)
+
+def p_type_name(p):
+    '''type_name : specifier_qualifier_list
+                 | specifier_qualifier_list abstract_declarator
+    '''
+
+def p_abstract_declarator(p):
+    '''abstract_declarator : pointer
+                           | direct_abstract_declarator
+                           | pointer direct_abstract_declarator
+    '''
+    if len(p) == 2:
+        p[0] = p[1]
+        if type(p[0]) == Pointer:
+            ptr = p[0]
+            while ptr.pointer:
+                ptr = ptr.pointer
+            # Only if doesn't already terminate in a declarator
+            if type(ptr) == Pointer:
+                ptr.pointer = Declarator()
+    else:
+        p[0] = p[1]
+        ptr = p[0]
+        while ptr.pointer:
+            ptr = ptr.pointer
+        ptr.pointer = p[2]
+
+def p_direct_abstract_declarator(p):
+    '''direct_abstract_declarator : '(' abstract_declarator ')'
+                      | '[' ']'
+                      | '[' constant_expression ']'
+                      | direct_abstract_declarator '[' ']'
+                      | direct_abstract_declarator '[' constant_expression ']'
+                      | '(' ')'
+                      | '(' parameter_type_list ')'
+                      | direct_abstract_declarator '(' ')'
+                      | direct_abstract_declarator '(' parameter_type_list ')'
+    '''
+    if p[1] == '(' and isinstance(p[2], Declarator):
+        p[0] = p[2]
+    else:
+        if isinstance(p[1], Declarator):
+            p[0] = p[1]
+            if p[2] == '[':
+                a = Array()
+                a.array = p[0].array
+                p[0].array = a
+                if p[3] != ']':
+                    p[0].array.size = p[3]
+            elif p[2] == '(':
+                if p[3] == ')':
+                    p[0].parameters = ()
+                else:
+                    p[0].parameters = p[3]
+        else:
+            p[0] = Declarator()
+            if p[1] == '[':
+                p[0].array = Array()
+                if p[2] != ']':
+                    p[0].array.size = p[2]
+            elif p[1] == '(':
+                if p[2] == ')':
+                    p[0].parameters = ()
+                else:
+                    p[0].parameters = p[2]
+
+def p_initializer(p):
+    '''initializer : assignment_expression
+                   | '{' initializer_list '}'
+                   | '{' initializer_list ',' '}'
+    '''
+
+def p_initializer_list(p):
+    '''initializer_list : initializer
+                        | initializer_list ',' initializer
+    '''
+
+def p_statement(p):
+    '''statement : labeled_statement
+                 | compound_statement
+                 | expression_statement
+                 | selection_statement
+                 | iteration_statement
+                 | jump_statement
+    '''
+
+def p_labeled_statement(p):
+    '''labeled_statement : IDENTIFIER ':' statement
+                         | CASE constant_expression ':' statement
+                         | DEFAULT ':' statement
+    '''
+
+def p_compound_statement(p):
+    '''compound_statement : '{' '}'
+                          | '{' statement_list '}'
+                          | '{' declaration_list '}'
+                          | '{' declaration_list statement_list '}'
+    '''
+
+def p_compound_statement_error(p):
+    '''compound_statement : '{' error '}'
+    '''
+    # Error resynchronisation catch-all
+
+def p_declaration_list(p):
+    '''declaration_list : declaration
+                        | declaration_list declaration
+    '''
+
+def p_statement_list(p):
+    '''statement_list : statement
+                      | statement_list statement
+    '''
+
+def p_expression_statement(p):
+    '''expression_statement : ';'
+                            | expression ';'
+    '''
+def p_expression_statement_error(p):
+    '''expression_statement : error ';'
+    '''
+    # Error resynchronisation catch-all
+
+def p_selection_statement(p):
+    '''selection_statement : IF '(' expression ')' statement
+                           | IF '(' expression ')' statement ELSE statement
+                           | SWITCH '(' expression ')' statement
+    '''
+
+def p_iteration_statement(p):
+    '''iteration_statement : WHILE '(' expression ')' statement
+    | DO statement WHILE '(' expression ')' ';'
+    | FOR '(' expression_statement expression_statement ')' statement
+    | FOR '(' expression_statement expression_statement expression ')' statement
+    '''	
+
+def p_jump_statement(p):
+    '''jump_statement : GOTO IDENTIFIER ';'
+                      | CONTINUE ';'
+                      | BREAK ';'
+                      | RETURN ';'
+                      | RETURN expression ';'
+    '''
+
+def p_external_declaration(p):
+    '''external_declaration : declaration 
+                            | function_definition
+    '''
+
+    # Intentionally empty
+
+def p_function_definition(p):
+    '''function_definition : declaration_specifiers declarator declaration_list compound_statement
+                        | declaration_specifiers declarator compound_statement
+                        | declarator declaration_list compound_statement
+                        | declarator compound_statement
+    '''
+
+def p_error(t):
+    if not t:
+        # Crap, no way to get to CParser instance.  FIXME TODO
+        print('Syntax error at end of file.', file=sys.stderr)
+    else:
+        t.lexer.cparser.handle_error('Syntax error at %r' % t.value, 
+             t.filename, t.lineno)
+    # Don't alter lexer: default behaviour is to pass error production
+    # up until it hits the catch-all at declaration, at which point
+    # parsing continues (synchronisation).
+
+# --------------------------------------------------------------------------
+# Lexer
+# --------------------------------------------------------------------------
+
+class CLexer(object):
+    def __init__(self, cparser):
+        self.cparser = cparser
+        self.type_names = set()
+
+    def input(self, tokens):
+        self.tokens = tokens
+        self.pos = 0
+
+    def token(self):
+        while self.pos < len(self.tokens):
+            t = self.tokens[self.pos]
+            self.pos += 1
+
+            if not t:
+                break
+
+            # PP events
+            if t.type == 'PP_DEFINE':
+                name, value = t.value
+                self.cparser.handle_define(
+                    name, value, t.filename, t.lineno)
+                continue
+            elif t.type == 'PP_DEFINE_CONSTANT':
+                name, value = t.value
+                self.cparser.handle_define_constant(
+                    name, value, t.filename, t.lineno)
+                continue
+            elif t.type == 'PP_IFNDEF':
+                self.cparser.handle_ifndef(t.value, t.filename, t.lineno)
+                continue
+            # TODO: other PP tokens
+
+            # Transform PP tokens into C tokens
+            if t.type == 'LPAREN':
+                t.type = '('
+            elif t.type == 'PP_NUMBER':
+                t.type = 'CONSTANT'
+            elif t.type == 'IDENTIFIER' and t.value in keywords:
+                t.type = t.value.upper()
+            elif t.type == 'IDENTIFIER' and t.value in self.type_names:
+                t.type = 'TYPE_NAME'
+            t.lexer = self
+            return t
+        return None
+        
+# --------------------------------------------------------------------------
+# Parser
+# --------------------------------------------------------------------------
+
+class CPreprocessorParser(preprocessor.PreprocessorParser):
+    def __init__(self, cparser, **kwargs):
+        self.cparser = cparser
+        preprocessor.PreprocessorParser.__init__(self, **kwargs)
+
+    def push_file(self, filename, data=None):
+        if not self.cparser.handle_include(filename):
+            return
+
+        tokens = self.cparser.get_cached_tokens(filename)
+        if tokens is not None:
+            self.output += tokens
+            return
+
+        if not data:
+            data = open(filename).read()
+        self.lexer.push_input(data, filename)
+
+class CParser(object):
+    '''Parse a C source file.
+
+    Subclass and override the handle_* methods.  Call `parse` with a string
+    to parse.
+    '''
+    def __init__(self, stddef_types=True, gnu_types=True, cache_headers=True):
+        self.preprocessor_parser = CPreprocessorParser(self)
+        self.parser = yacc.Parser()
+        yacc.yacc(method='LALR').init_parser(self.parser)
+        self.parser.cparser = self
+
+        self.lexer = CLexer(self)
+        if stddef_types:
+            self.lexer.type_names.add('wchar_t')
+            self.lexer.type_names.add('ptrdiff_t')
+            self.lexer.type_names.add('size_t')
+        if gnu_types:
+            self.lexer.type_names.add('__builtin_va_list')
+        if sys.platform == 'win32':
+            self.lexer.type_names.add('__int64')
+
+        self.header_cache = {}
+        self.cache_headers = cache_headers
+        self.load_header_cache()
+    
+    def parse(self, filename, data=None, debug=False):
+        '''Parse a file.  Give filename or filename + data.
+
+        If `debug` is True, parsing state is dumped to stdout.
+        '''
+        if not data:
+            data = open(filename, 'r').read()
+        
+        self.handle_status('Preprocessing %s' % filename)
+        self.preprocessor_parser.parse(filename, data, debug=debug)
+        self.lexer.input(self.preprocessor_parser.output)
+        self.handle_status('Parsing %s' % filename)
+        self.parser.parse(lexer=self.lexer, debug=debug)
+
+    def load_header_cache(self, filename=None):
+        if not filename:
+            filename = '.header.cache'
+        try:
+            self.header_cache = cPickle.load(open(filename, 'rb'))
+            self.handle_status('Loaded header cache "%s".  Found:' % filename)
+            for header in self.header_cache.keys():
+                self.handle_status('  %s' % header)
+        except:
+            self.handle_status('Failed to load header cache "%s"' % filename)
+
+    def save_header_cache(self, filename=None):
+        if not filename:
+            filename = '.header.cache'
+        try:
+            cPickle.dump(self.header_cache, open(filename, 'wb'))
+            self.handle_status('Updated header cache "%s"' % filename)
+        except:
+            self.handle_status('Failed to update header cache "%s"' % filename)
+
+    def get_cached_tokens(self, header):
+        '''Return a list of tokens for `header`.
+
+        If there is no cached copy, return None.
+        '''
+        try:
+            now = os.stat(header).st_mtime
+        except OSError:
+            now = time.time()
+        current_memento = self.preprocessor_parser.get_memento()
+        if header in self.header_cache:
+            timestamp, memento, tokens, namespace = self.header_cache[header]
+            if self.preprocessor_parser.system_headers:
+                self.handle_status('Not using cached header "%s" because ' \
+                                   'of overridden system_headers.' % header)
+            elif now < timestamp:
+                self.handle_status('Not using cached header "%s" because ' \
+                                   'cached copy is stale.' % header)
+            elif memento != current_memento:
+                self.handle_status('Not using cached header "%s" because ' \
+                                   'memento differs.' % header)
+            else:
+                self.handle_status('Using cached header "%s"' % header)
+                self.preprocessor_parser.namespace = namespace
+                return tokens
+
+        if self.cache_headers and not self.preprocessor_parser.system_headers:
+            self.handle_status('Caching header "%s"' % header)
+            self.cache_headers = False
+            ppp = preprocessor.PreprocessorParser()
+            ppp.include_path = self.preprocessor_parser.include_path
+            ppp.parse(filename=header,
+                      namespace=self.preprocessor_parser.namespace)
+            self.header_cache[header] = (now, current_memento, 
+                                         ppp.output, ppp.namespace.copy())
+            self.save_header_cache()
+            self.cache_headers = True
+            return ppp.output
+
+        return None
+
+    # ----------------------------------------------------------------------
+    # Parser interface.  Override these methods in your subclass.
+    # ----------------------------------------------------------------------
+
+    def handle_error(self, message, filename, lineno):
+        '''A parse error occured.  
+        
+        The default implementation prints `lineno` and `message` to stderr.
+        The parser will try to recover from errors by synchronising at the
+        next semicolon.
+        '''
+        print('%s:%s %s' % (filename, lineno, message), file=sys.stderr)
+
+    def handle_status(self, message):
+        '''Progress information.
+
+        The default implementationg prints message to stderr.
+        '''
+        print(message, file=sys.stderr)
+
+    def handle_include(self, header):
+        '''#include `header`
+        
+        Return True to proceed with including the header, otherwise return
+        False to skip it.  The default implementation returns True.
+        '''
+        return True
+
+    def handle_define(self, name, value, filename, lineno):
+        '''#define `name` `value` 
+
+        both are strings, value could not be parsed as an expression
+        '''
+
+    def handle_define_constant(self, name, value, filename, lineno):
+        '''#define `name` `value`
+
+        value is an int or float
+        '''
+
+    def handle_undef(self, name):
+        '''#undef `name`'''
+
+    def handle_if(self, expr):
+        '''#if `expr`'''
+
+    def handle_ifdef(self, name):
+        '''#ifdef `name`'''
+
+    def handle_ifndef(self, name, filename, lineno):
+        '''#ifndef `name`'''
+
+    def handle_elif(self, expr):
+        '''#elif `expr`'''
+
+    def handle_else(self):
+        '''#else'''
+
+    def handle_endif(self):
+        '''#endif'''
+
+    def impl_handle_declaration(self, declaration, filename, lineno):
+        '''Internal method that calls `handle_declaration`.  This method
+        also adds any new type definitions to the lexer's list of valid type
+        names, which affects the parsing of subsequent declarations.
+        '''
+        if declaration.storage == 'typedef':
+            declarator = declaration.declarator
+            if not declarator:
+                # XXX TEMPORARY while struct etc not filled
+                return
+            while declarator.pointer:
+                declarator = declarator.pointer
+            self.lexer.type_names.add(declarator.identifier)
+        self.handle_declaration(declaration, filename, lineno)
+
+    def handle_declaration(self, declaration, filename, lineno):
+        '''A declaration was encountered.  
+        
+        `declaration` is an instance of Declaration.  Where a declaration has
+        multiple initialisers, each is returned as a separate declaration.
+        '''
+        pass
+
+class DebugCParser(CParser):
+    '''A convenience class that prints each invocation of a handle_* method to
+    stdout.
+    '''
+    def handle_include(self, header):
+        print('#include header=%r' % header)
+        return True
+
+    def handle_define(self, name, value, filename, lineno):
+        print('#define name=%r, value=%r' % (name, value))
+
+    def handle_define_constant(self, name, value, filename, lineno):
+        print('#define constant name=%r, value=%r' % (name, value))
+
+    def handle_undef(self, name):
+        print('#undef name=%r' % name)
+
+    def handle_if(self, expr):
+        print('#if expr=%s' % expr)
+
+    def handle_ifdef(self, name):
+        print('#ifdef name=%r' % name)
+
+    def handle_ifndef(self, name, filename, lineno):
+        print('#ifndef name=%r' % name)
+
+    def handle_elif(self, expr):
+        print('#elif expr=%s' % expr)
+
+    def handle_else(self):
+        print('#else')
+
+    def handle_endif(self):
+        print('#endif')
+
+    def handle_declaration(self, declaration, filename, lineno):
+        print(declaration)
+        
+if __name__ == '__main__':
+    DebugCParser().parse(sys.argv[1], debug=True)
diff --git a/tools/wraptypes/ctypesparser.py b/tools/wraptypes/ctypesparser.py
new file mode 100644
index 0000000..fdd5c8b
--- /dev/null
+++ b/tools/wraptypes/ctypesparser.py
@@ -0,0 +1,321 @@
+#!/usr/bin/env python
+
+'''
+'''
+
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+from cparser import *
+
+ctypes_type_map = {
+     # typename signed  longs
+    ('void',    True,   0): 'None',
+    ('int',     True,   0): 'c_int',
+    ('int',     False,  0): 'c_uint',
+    ('int',     True,   1): 'c_long',
+    ('int',     False,  1): 'c_ulong',
+    ('int',     True,   2): 'c_longlong',
+    ('int',     False,  2): 'c_ulonglong',
+    ('char',    True,   0): 'c_char',
+    ('char',    False,  0): 'c_ubyte',
+    ('short',   True,   0): 'c_short',
+    ('short',   False,  0): 'c_ushort',
+    ('float',   True,   0): 'c_float',
+    ('double',  True,   0): 'c_double',
+    ('size_t',  True,   0): 'c_size_t',
+    ('int8_t',  True,   0): 'c_int8',
+    ('int16_t', True,   0): 'c_int16',
+    ('int32_t', True,   0): 'c_int32',
+    ('int64_t', True,   0): 'c_int64',
+    ('uint8_t', True,   0): 'c_uint8',
+    ('uint16_t',True,   0): 'c_uint16',
+    ('uint32_t',True,   0): 'c_uint32',
+    ('uint64_t',True,   0): 'c_uint64',
+    ('wchar_t', True,   0): 'c_wchar',
+    ('ptrdiff_t',True,  0): 'c_ptrdiff_t',  # Requires definition in preamble
+}
+
+reserved_names = ['None', 'True', 'False']
+
+def get_ctypes_type(typ, declarator):
+    signed = True
+    typename = 'int'
+    longs = 0
+    t = None
+    for specifier in typ.specifiers:
+        if isinstance(specifier, StructTypeSpecifier):
+            t = CtypesStruct(specifier)
+        elif isinstance(specifier, EnumSpecifier):
+            t = CtypesEnum(specifier)
+        elif specifier == 'signed':
+            signed = True
+        elif specifier == 'unsigned':
+            signed = False
+        elif specifier == 'long':
+            longs += 1
+        else:
+            typename = str(specifier)
+    if not t:
+        ctypes_name = ctypes_type_map.get((typename, signed, longs), typename)
+        t = CtypesType(ctypes_name)
+
+    while declarator and declarator.pointer:
+        if declarator.parameters is not None:
+            t = CtypesFunction(t, declarator.parameters)
+        a = declarator.array
+        while a:
+            t = CtypesArray(t, a.size)
+            a = a.array
+
+        if type(t) == CtypesType and t.name == 'c_char':
+            t = CtypesType('c_char_p')
+        elif type(t) == CtypesType and t.name == 'c_wchar':
+            t = CtypesType('c_wchar_p')
+        else:
+            t = CtypesPointer(t, declarator.qualifiers)
+        declarator = declarator.pointer
+    if declarator and declarator.parameters is not None:
+        t = CtypesFunction(t, declarator.parameters)
+    if declarator:
+        a = declarator.array
+        while a:
+            t = CtypesArray(t, a.size)
+            a = a.array
+    return t
+    
+# Remove one level of indirection from funtion pointer; needed for typedefs
+# and function parameters.
+def remove_function_pointer(t):
+    if type(t) == CtypesPointer and type(t.destination) == CtypesFunction:
+        return t.destination
+    elif type(t) == CtypesPointer:
+        t.destination = remove_function_pointer(t.destination)
+        return t
+    else:
+        return t
+
+class CtypesTypeVisitor(object):
+    def visit_struct(self, struct):
+        pass
+
+    def visit_enum(self, enum):
+        pass
+
+class CtypesType(object):
+    def __init__(self, name):
+        self.name = name
+
+    def get_required_type_names(self):
+        '''Return all type names defined or needed by this type'''
+        return (self.name,)
+
+    def visit(self, visitor):
+        pass
+
+    def __str__(self):
+        return self.name
+
+class CtypesPointer(CtypesType):
+    def __init__(self, destination, qualifiers):
+        self.destination = destination
+        # ignore qualifiers, ctypes can't use them
+
+    def get_required_type_names(self):
+        if self.destination:
+            return self.destination.get_required_type_names()
+        else:
+            return ()
+
+    def visit(self, visitor):
+        if self.destination:
+            self.destination.visit(visitor)
+
+    def __str__(self):
+        return 'POINTER(%s)' % str(self.destination)
+
+class CtypesArray(CtypesType):
+    def __init__(self, base, count):
+        self.base = base
+        self.count = count
+    
+    def get_required_type_names(self):
+        # XXX Could be sizeofs within count expression
+        return self.base.get_required_type_names()
+ 
+    def visit(self, visitor):
+        # XXX Could be sizeofs within count expression
+        self.base.visit(visitor)
+
+    def __str__(self):
+        if self.count is None:
+            return 'POINTER(%s)' % str(self.base)
+        if type(self.base) == CtypesArray:
+            return '(%s) * %s' % (str(self.base), str(self.count))
+        else:
+            return '%s * %s' % (str(self.base), str(self.count))
+
+class CtypesFunction(CtypesType):
+    def __init__(self, restype, parameters):
+        if parameters and parameters[-1] == '...':
+            # XXX Hmm, how to handle VARARGS with ctypes?  For now,
+            # drop it off (will cause errors).
+            parameters = parameters[:-1]
+            
+        self.restype = restype
+
+        # Don't allow POINTER(None) (c_void_p) as a restype... causes errors
+        # when ctypes automagically returns it as an int.
+        # Instead, convert to POINTER(c_void).  c_void is not a ctypes type,
+        # you can make it any arbitrary type.
+        if type(self.restype) == CtypesPointer and \
+           type(self.restype.destination) == CtypesType and \
+           self.restype.destination.name == 'None':
+            self.restype = CtypesPointer(CtypesType('c_void'), ())
+
+        self.argtypes = [remove_function_pointer(
+                            get_ctypes_type(p.type, p.declarator)) \
+                         for p in parameters]
+
+    def get_required_type_names(self):
+        lst = list(self.restype.get_required_type_names())
+        for a in self.argtypes:
+            lst += list(a.get_required_type_names())
+        return lst
+
+    def visit(self, visitor):
+        self.restype.visit(visitor)
+        for a in self.argtypes:
+            a.visit(visitor)
+
+    def __str__(self):
+        return 'CFUNCTYPE(%s)' % ', '.join([str(self.restype)] + \
+            [str(a) for a in self.argtypes])
+
+last_tagnum = 0
+def anonymous_struct_tag():
+    global last_tagnum
+    last_tagnum += 1
+    return 'anon_%d' % last_tagnum
+
+class CtypesStruct(CtypesType):
+    def __init__(self, specifier):
+        self.is_union = specifier.is_union
+        self.tag = specifier.tag
+        if not self.tag:
+            self.tag = anonymous_struct_tag()
+
+        if specifier.declarations:
+            self.opaque = False
+            self.members = []
+            for declaration in specifier.declarations:
+                t = get_ctypes_type(declaration.type, declaration.declarator)
+                declarator = declaration.declarator
+                if declarator is None:
+                    # XXX TEMPORARY while struct with no typedef not filled in
+                    return
+                while declarator.pointer:
+                    declarator = declarator.pointer
+                name = declarator.identifier
+                self.members.append((name, t))
+        else:
+            self.opaque = True
+            self.members = []
+
+    def get_required_type_names(self):
+        lst = ['struct_%s' % self.tag]
+        for m in self.members:
+            lst += m[1].get_required_type_names()
+        return lst
+
+    def visit(self, visitor):
+        visitor.visit_struct(self)
+
+    def __str__(self):
+        return 'struct_%s' % self.tag
+
+last_tagnum = 0
+def anonymous_enum_tag():
+    global last_tagnum
+    last_tagnum += 1
+    return 'anon_%d' % last_tagnum
+
+class CtypesEnum(CtypesType):
+    def __init__(self, specifier):
+        self.tag = specifier.tag
+        if not self.tag:
+            self.tag = anonymous_enum_tag()
+
+        value = 0
+        context = EvaluationContext()
+        self.enumerators = []
+        for e in specifier.enumerators:
+            if e.expression:
+                try:
+                    value = int(e.expression.evaluate(context))
+                except:
+                    pass
+            self.enumerators.append((e.name, value))
+            value += 1
+
+    def get_required_type_names(self):
+        return []
+
+    def visit(self, visitor):
+        visitor.visit_enum(self)
+
+    def __str__(self):
+        return 'enum_%s' % self.tag
+
+class CtypesParser(CParser):
+    '''Parse a C file for declarations that can be used by ctypes.
+    
+    Subclass and override the handle_ctypes_* methods.
+    '''
+    def handle_define(self, name, value, filename, lineno):
+        # Handle #define style of typedeffing.
+        # XXX At the moment, just a hack for `int`, which is used by
+        # Status and Bool in Xlib.h.  More complete functionality would
+        # parse value as a type (back into cparser).
+        if value == 'int':
+            t = CtypesType('c_int')
+            self.handle_ctypes_type_definition(
+                name, t, filename, lineno)
+
+    def handle_define_constant(self, name, value, filename, lineno):
+        if name in reserved_names:
+            name += '_'
+        self.handle_ctypes_constant(name, value, filename, lineno)
+
+    def handle_declaration(self, declaration, filename, lineno):
+        t = get_ctypes_type(declaration.type, declaration.declarator)
+        declarator = declaration.declarator
+        if declarator is None:
+            # XXX TEMPORARY while struct with no typedef not filled in
+            return
+        while declarator.pointer:
+            declarator = declarator.pointer
+        name = declarator.identifier
+        if declaration.storage == 'typedef':
+            self.handle_ctypes_type_definition(
+                name, remove_function_pointer(t), filename, lineno)
+        elif type(t) == CtypesFunction:
+            self.handle_ctypes_function(
+                name, t.restype, t.argtypes, filename, lineno)
+        elif declaration.storage != 'static':
+            self.handle_ctypes_variable(name, t, filename, lineno)
+
+    # ctypes parser interface.  Override these methods in your subclass.
+
+    def handle_ctypes_constant(self, name, value, filename, lineno):
+        pass
+
+    def handle_ctypes_type_definition(self, name, ctype, filename, lineno):
+        pass
+
+    def handle_ctypes_function(self, name, restype, argtypes, filename, lineno):
+        pass
+
+    def handle_ctypes_variable(self, name, ctype, filename, lineno):
+        pass
+
diff --git a/tools/wraptypes/lex.py b/tools/wraptypes/lex.py
new file mode 100644
index 0000000..c542189
--- /dev/null
+++ b/tools/wraptypes/lex.py
@@ -0,0 +1,877 @@
+from __future__ import print_function
+#-----------------------------------------------------------------------------
+# ply: lex.py
+#
+# Author: David M. Beazley (dave@dabeaz.com)
+# Modification for pyglet by Alex Holkner (alex.holkner@gmail.com)
+#
+# Copyright (C) 2001-2006, David M. Beazley
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+# See the file LICENSE for a complete copy of the LGPL.
+#-----------------------------------------------------------------------------
+
+__version__ = "2.2"
+
+import re, sys, types
+
+# Regular expression used to match valid token names
+_is_identifier = re.compile(r'^[a-zA-Z0-9_]+$')
+
+# Available instance types.  This is used when lexers are defined by a class.
+# It's a little funky because I want to preserve backwards compatibility
+# with Python 2.0 where types.ObjectType is undefined.
+
+try:
+   _INSTANCETYPE = (types.InstanceType, types.ObjectType)
+except AttributeError:
+   _INSTANCETYPE = types.InstanceType
+   class object: pass       # Note: needed if no new-style classes present
+
+# Exception thrown when invalid token encountered and no default error
+# handler is defined.
+class LexError(Exception):
+    def __init__(self,message,s):
+         self.args = (message,)
+         self.text = s
+
+# Token class
+class LexToken(object):
+    def __str__(self):
+        return "LexToken(%s,%r,%d,%d)" % (self.type,self.value,self.lineno,self.lexpos)
+    def __repr__(self):
+        return str(self)
+    def skip(self,n):
+        self.lexer.skip(n)
+
+# -----------------------------------------------------------------------------
+# Lexer class
+#
+# This class encapsulates all of the methods and data associated with a lexer.
+#
+#    input()          -  Store a new string in the lexer
+#    token()          -  Get the next token
+# -----------------------------------------------------------------------------
+
+class Lexer:
+    def __init__(self):
+        self.lexre = None             # Master regular expression. This is a list of
+                                      # tuples (re,findex) where re is a compiled
+                                      # regular expression and findex is a list
+                                      # mapping regex group numbers to rules
+        self.lexretext = None         # Current regular expression strings
+        self.lexstatere = {}          # Dictionary mapping lexer states to master regexs
+        self.lexstateretext = {}      # Dictionary mapping lexer states to regex strings
+        self.lexstate = "INITIAL"     # Current lexer state
+        self.lexstatestack = []       # Stack of lexer states
+        self.lexstateinfo = None      # State information
+        self.lexstateignore = {}      # Dictionary of ignored characters for each state
+        self.lexstateerrorf = {}      # Dictionary of error functions for each state
+        self.lexreflags = 0           # Optional re compile flags
+        self.lexdata = None           # Actual input data (as a string)
+        self.lexpos = 0               # Current position in input text
+        self.lexlen = 0               # Length of the input text
+        self.lexerrorf = None         # Error rule (if any)
+        self.lextokens = None         # List of valid tokens
+        self.lexignore = ""           # Ignored characters
+        self.lexliterals = ""         # Literal characters that can be passed through
+        self.lexmodule = None         # Module
+        self.lineno = 1               # Current line number
+        self.lexdebug = 0             # Debugging mode
+        self.lexoptimize = 0          # Optimized mode
+
+    def clone(self,object=None):
+        c = Lexer()
+        c.lexstatere = self.lexstatere
+        c.lexstateinfo = self.lexstateinfo
+        c.lexstateretext = self.lexstateretext
+        c.lexstate = self.lexstate
+        c.lexstatestack = self.lexstatestack
+        c.lexstateignore = self.lexstateignore
+        c.lexstateerrorf = self.lexstateerrorf
+        c.lexreflags = self.lexreflags
+        c.lexdata = self.lexdata
+        c.lexpos = self.lexpos
+        c.lexlen = self.lexlen
+        c.lextokens = self.lextokens
+        c.lexdebug = self.lexdebug
+        c.lineno = self.lineno
+        c.lexoptimize = self.lexoptimize
+        c.lexliterals = self.lexliterals
+        c.lexmodule   = self.lexmodule
+
+        # If the object parameter has been supplied, it means we are attaching the
+        # lexer to a new object.  In this case, we have to rebind all methods in
+        # the lexstatere and lexstateerrorf tables.
+
+        if object:
+            newtab = { }
+            for key, ritem in self.lexstatere.items():
+                newre = []
+                for cre, findex in ritem:
+                     newfindex = []
+                     for f in findex:
+                         if not f or not f[0]:
+                             newfindex.append(f)
+                             continue
+                         newfindex.append((getattr(object,f[0].__name__),f[1]))
+                newre.append((cre,newfindex))
+                newtab[key] = newre
+            c.lexstatere = newtab
+            c.lexstateerrorf = { }
+            for key, ef in self.lexstateerrorf.items():
+                c.lexstateerrorf[key] = getattr(object,ef.__name__)
+            c.lexmodule = object
+
+        # Set up other attributes
+        c.begin(c.lexstate)
+        return c
+
+    # ------------------------------------------------------------
+    # writetab() - Write lexer information to a table file
+    # ------------------------------------------------------------
+    def writetab(self,tabfile):
+        tf = open(tabfile+".py","w")
+        tf.write("# %s.py. This file automatically created by PLY (version %s). Don't edit!\n" % (tabfile,__version__))
+        tf.write("_lextokens    = %s\n" % repr(self.lextokens))
+        tf.write("_lexreflags   = %s\n" % repr(self.lexreflags))
+        tf.write("_lexliterals  = %s\n" % repr(self.lexliterals))
+        tf.write("_lexstateinfo = %s\n" % repr(self.lexstateinfo))
+
+        tabre = { }
+        for key, lre in self.lexstatere.items():
+             titem = []
+             for i in range(len(lre)):
+                  titem.append((self.lexstateretext[key][i],_funcs_to_names(lre[i][1])))
+             tabre[key] = titem
+
+        tf.write("_lexstatere   = %s\n" % repr(tabre))
+        tf.write("_lexstateignore = %s\n" % repr(self.lexstateignore))
+
+        taberr = { }
+        for key, ef in self.lexstateerrorf.items():
+             if ef:
+                  taberr[key] = ef.__name__
+             else:
+                  taberr[key] = None
+        tf.write("_lexstateerrorf = %s\n" % repr(taberr))
+        tf.close()
+
+    # ------------------------------------------------------------
+    # readtab() - Read lexer information from a tab file
+    # ------------------------------------------------------------
+    def readtab(self,tabfile,fdict):
+        exec("import %s as lextab" % tabfile)
+        global lextab  # declare the name of the imported module
+        self.lextokens      = lextab._lextokens
+        self.lexreflags     = lextab._lexreflags
+        self.lexliterals    = lextab._lexliterals
+        self.lexstateinfo   = lextab._lexstateinfo
+        self.lexstateignore = lextab._lexstateignore
+        self.lexstatere     = { }
+        self.lexstateretext = { }
+        for key,lre in lextab._lexstatere.items():
+             titem = []
+             txtitem = []
+             for i in range(len(lre)):
+                  titem.append((re.compile(lre[i][0],lextab._lexreflags),_names_to_funcs(lre[i][1],fdict)))
+                  txtitem.append(lre[i][0])
+             self.lexstatere[key] = titem
+             self.lexstateretext[key] = txtitem
+        self.lexstateerrorf = { }
+        for key,ef in lextab._lexstateerrorf.items():
+             self.lexstateerrorf[key] = fdict[ef]
+        self.begin('INITIAL')
+
+    # ------------------------------------------------------------
+    # input() - Push a new string into the lexer
+    # ------------------------------------------------------------
+    def input(self,s):
+        if not (isinstance(s,types.StringType) or isinstance(s,types.UnicodeType)):
+            raise ValueError("Expected a string")
+        self.lexdata = s
+        self.lexpos = 0
+        self.lexlen = len(s)
+
+    # ------------------------------------------------------------
+    # begin() - Changes the lexing state
+    # ------------------------------------------------------------
+    def begin(self,state):
+        if not self.lexstatere.has_key(state):
+            raise ValueError("Undefined state")
+        self.lexre = self.lexstatere[state]
+        self.lexretext = self.lexstateretext[state]
+        self.lexignore = self.lexstateignore.get(state,"")
+        self.lexerrorf = self.lexstateerrorf.get(state,None)
+        self.lexstate = state
+
+    # ------------------------------------------------------------
+    # push_state() - Changes the lexing state and saves old on stack
+    # ------------------------------------------------------------
+    def push_state(self,state):
+        self.lexstatestack.append(self.lexstate)
+        self.begin(state)
+
+    # ------------------------------------------------------------
+    # pop_state() - Restores the previous state
+    # ------------------------------------------------------------
+    def pop_state(self):
+        self.begin(self.lexstatestack.pop())
+
+    # ------------------------------------------------------------
+    # current_state() - Returns the current lexing state
+    # ------------------------------------------------------------
+    def current_state(self):
+        return self.lexstate
+
+    # ------------------------------------------------------------
+    # skip() - Skip ahead n characters
+    # ------------------------------------------------------------
+    def skip(self,n):
+        self.lexpos += n
+
+    # ------------------------------------------------------------
+    # token() - Return the next token from the Lexer
+    #
+    # Note: This function has been carefully implemented to be as fast
+    # as possible.  Don't make changes unless you really know what
+    # you are doing
+    # ------------------------------------------------------------
+    def token(self):
+        # Make local copies of frequently referenced attributes
+        lexpos    = self.lexpos
+        lexlen    = self.lexlen
+        lexignore = self.lexignore
+        lexdata   = self.lexdata
+
+        while lexpos < lexlen:
+            # This code provides some short-circuit code for whitespace, tabs, and other ignored characters
+            if lexdata[lexpos] in lexignore:
+                lexpos += 1
+                continue
+
+            # Look for a regular expression match
+            for lexre,lexindexfunc in self.lexre:
+                m = lexre.match(lexdata,lexpos)
+                if not m: continue
+
+                # Set last match in lexer so that rules can access it if they want
+                self.lexmatch = m
+
+                # Create a token for return
+                tok = LexToken()
+                tok.value = m.group()
+                tok.lineno = self.lineno
+                tok.lexpos = lexpos
+                tok.lexer = self
+
+                lexpos = m.end()
+                i = m.lastindex
+                func,tok.type = lexindexfunc[i]
+                self.lexpos = lexpos
+
+                if not func:
+                   # If no token type was set, it's an ignored token
+                   if tok.type: return tok
+                   break
+
+                # if func not callable, it means it's an ignored token
+                if not callable(func):
+                   break
+
+                # If token is processed by a function, call it
+                newtok = func(tok)
+
+                # Every function must return a token, if nothing, we just move to next token
+                if not newtok:
+                    lexpos = self.lexpos        # This is here in case user has updated lexpos.
+
+                    # Added for pyglet/tools/wrapper/cparser.py by Alex
+                    # Holkner on 20/Jan/2007
+                    lexdata = self.lexdata
+                    break
+
+                # Verify type of the token.  If not in the token map, raise an error
+                if not self.lexoptimize:
+                    # Allow any single-character literal also for
+                    # pyglet/tools/wrapper/cparser.py by Alex Holkner on
+                    # 20/Jan/2007
+                    if not self.lextokens.has_key(newtok.type) and len(newtok.type) > 1:
+                        raise LexError("%s:%d: Rule '%s' returned an unknown token type '%s'" % (
+                            func.func_code.co_filename, func.func_code.co_firstlineno,
+                            func.__name__, newtok.type),lexdata[lexpos:])
+
+                return newtok
+            else:
+                # No match, see if in literals
+                if lexdata[lexpos] in self.lexliterals:
+                    tok = LexToken()
+                    tok.value = lexdata[lexpos]
+                    tok.lineno = self.lineno
+                    tok.lexer = self
+                    tok.type = tok.value
+                    tok.lexpos = lexpos
+                    self.lexpos = lexpos + 1
+                    return tok
+
+                # No match. Call t_error() if defined.
+                if self.lexerrorf:
+                    tok = LexToken()
+                    tok.value = self.lexdata[lexpos:]
+                    tok.lineno = self.lineno
+                    tok.type = "error"
+                    tok.lexer = self
+                    tok.lexpos = lexpos
+                    self.lexpos = lexpos
+                    newtok = self.lexerrorf(tok)
+                    if lexpos == self.lexpos:
+                        # Error method didn't change text position at all. This is an error.
+                        raise LexError("Scanning error. Illegal character '%s'" % (lexdata[lexpos]), lexdata[lexpos:])
+                    lexpos = self.lexpos
+                    if not newtok: continue
+                    return newtok
+
+                self.lexpos = lexpos
+                raise LexError("Illegal character '%s' at index %d" % (lexdata[lexpos],lexpos), lexdata[lexpos:])
+
+        self.lexpos = lexpos + 1
+        if self.lexdata is None:
+             raise RuntimeError("No input string given with input()")
+        return None
+
+# -----------------------------------------------------------------------------
+# _validate_file()
+#
+# This checks to see if there are duplicated t_rulename() functions or strings
+# in the parser input file.  This is done using a simple regular expression
+# match on each line in the filename.
+# -----------------------------------------------------------------------------
+
+def _validate_file(filename):
+    import os.path
+    base,ext = os.path.splitext(filename)
+    if ext != '.py': return 1        # No idea what the file is. Return OK
+
+    try:
+        f = open(filename)
+        lines = f.readlines()
+        f.close()
+    except IOError:
+        return 1                       # Oh well
+
+    fre = re.compile(r'\s*def\s+(t_[a-zA-Z_0-9]*)\(')
+    sre = re.compile(r'\s*(t_[a-zA-Z_0-9]*)\s*=')
+    counthash = { }
+    linen = 1
+    noerror = 1
+    for l in lines:
+        m = fre.match(l)
+        if not m:
+            m = sre.match(l)
+        if m:
+            name = m.group(1)
+            prev = counthash.get(name)
+            if not prev:
+                counthash[name] = linen
+            else:
+                print("%s:%d: Rule %s redefined. Previously defined on line %d" % (filename,linen,name,prev))
+                noerror = 0
+        linen += 1
+    return noerror
+
+# -----------------------------------------------------------------------------
+# _funcs_to_names()
+#
+# Given a list of regular expression functions, this converts it to a list
+# suitable for output to a table file
+# -----------------------------------------------------------------------------
+
+def _funcs_to_names(funclist):
+    result = []
+    for f in funclist:
+         if f and f[0]:
+             result.append((f[0].__name__,f[1]))
+         else:
+             result.append(f)
+    return result
+
+# -----------------------------------------------------------------------------
+# _names_to_funcs()
+#
+# Given a list of regular expression function names, this converts it back to
+# functions.
+# -----------------------------------------------------------------------------
+
+def _names_to_funcs(namelist,fdict):
+     result = []
+     for n in namelist:
+          if n and n[0]:
+              result.append((fdict[n[0]],n[1]))
+          else:
+              result.append(n)
+     return result
+
+# -----------------------------------------------------------------------------
+# _form_master_re()
+#
+# This function takes a list of all of the regex components and attempts to
+# form the master regular expression.  Given limitations in the Python re
+# module, it may be necessary to break the master regex into separate expressions.
+# -----------------------------------------------------------------------------
+
+def _form_master_re(relist,reflags,ldict):
+    if not relist: return []
+    regex = "|".join(relist)
+    try:
+        lexre = re.compile(regex,re.VERBOSE | reflags)
+
+        # Build the index to function map for the matching engine
+        lexindexfunc = [ None ] * (max(lexre.groupindex.values())+1)
+        for f,i in lexre.groupindex.items():
+            handle = ldict.get(f,None)
+            if type(handle) in (types.FunctionType, types.MethodType):
+                lexindexfunc[i] = (handle,handle.__name__[2:])
+            elif handle is not None:
+                # If rule was specified as a string, we build an anonymous
+                # callback function to carry out the action
+                if f.find("ignore_") > 0:
+                    lexindexfunc[i] = (None,None)
+                    print("IGNORE", f)
+                else:
+                    lexindexfunc[i] = (None, f[2:])
+
+        return [(lexre,lexindexfunc)],[regex]
+    except Exception as e:
+        m = int(len(relist)/2)
+        if m == 0: m = 1
+        llist, lre = _form_master_re(relist[:m],reflags,ldict)
+        rlist, rre = _form_master_re(relist[m:],reflags,ldict)
+        return llist+rlist, lre+rre
+
+# -----------------------------------------------------------------------------
+# def _statetoken(s,names)
+#
+# Given a declaration name s of the form "t_" and a dictionary whose keys are
+# state names, this function returns a tuple (states,tokenname) where states
+# is a tuple of state names and tokenname is the name of the token.  For example,
+# calling this with s = "t_foo_bar_SPAM" might return (('foo','bar'),'SPAM')
+# -----------------------------------------------------------------------------
+
+def _statetoken(s,names):
+    nonstate = 1
+    parts = s.split("_")
+    for i in range(1,len(parts)):
+         if not names.has_key(parts[i]) and parts[i] != 'ANY': break
+    if i > 1:
+       states = tuple(parts[1:i])
+    else:
+       states = ('INITIAL',)
+
+    if 'ANY' in states:
+       states = tuple(names.keys())
+
+    tokenname = "_".join(parts[i:])
+    return (states,tokenname)
+
+# -----------------------------------------------------------------------------
+# lex(module)
+#
+# Build all of the regular expression rules from definitions in the supplied module
+# -----------------------------------------------------------------------------
+# cls added for pyglet/tools/wrapper/cparser.py by Alex Holkner on 22/Jan/2007
+def lex(module=None,object=None,debug=0,optimize=0,lextab="lextab",reflags=0,nowarn=0,cls=Lexer):
+    global lexer
+    ldict = None
+    stateinfo  = { 'INITIAL' : 'inclusive'}
+    error = 0
+    files = { }
+    lexobj = cls()
+    lexobj.lexdebug = debug
+    lexobj.lexoptimize = optimize
+    global token,input
+
+    if nowarn: warn = 0
+    else: warn = 1
+
+    if object: module = object
+
+    if module:
+        # User supplied a module object.
+        if isinstance(module, types.ModuleType):
+            ldict = module.__dict__
+        elif isinstance(module, _INSTANCETYPE):
+            _items = [(k,getattr(module,k)) for k in dir(module)]
+            ldict = { }
+            for (i,v) in _items:
+                ldict[i] = v
+        else:
+            raise ValueError("Expected a module or instance")
+        lexobj.lexmodule = module
+
+    else:
+        # No module given.  We might be able to get information from the caller.
+        try:
+            raise RuntimeError
+        except RuntimeError:
+            e,b,t = sys.exc_info()
+            f = t.tb_frame
+            f = f.f_back           # Walk out to our calling function
+            ldict = f.f_globals    # Grab its globals dictionary
+
+    if optimize and lextab:
+        try:
+            lexobj.readtab(lextab,ldict)
+            token = lexobj.token
+            input = lexobj.input
+            lexer = lexobj
+            return lexobj
+
+        except ImportError:
+            pass
+
+    # Get the tokens, states, and literals variables (if any)
+    if (module and isinstance(module,_INSTANCETYPE)):
+        tokens   = getattr(module,"tokens",None)
+        states   = getattr(module,"states",None)
+        literals = getattr(module,"literals","")
+    else:
+        tokens   = ldict.get("tokens",None)
+        states   = ldict.get("states",None)
+        literals = ldict.get("literals","")
+
+    if not tokens:
+        raise SyntaxError("lex: module does not define 'tokens'")
+    if not (isinstance(tokens,types.ListType) or isinstance(tokens,types.TupleType)):
+        raise SyntaxError("lex: tokens must be a list or tuple.")
+
+    # Build a dictionary of valid token names
+    lexobj.lextokens = { }
+    if not optimize:
+        for n in tokens:
+            if not _is_identifier.match(n):
+                print("lex: Bad token name '%s'" % n)
+                error = 1
+            if warn and lexobj.lextokens.has_key(n):
+                print("lex: Warning. Token '%s' multiply defined." % n)
+            lexobj.lextokens[n] = None
+    else:
+        for n in tokens: lexobj.lextokens[n] = None
+
+    if debug:
+        print("lex: tokens = '%s'" % lexobj.lextokens.keys())
+
+    try:
+         for c in literals:
+               if not (isinstance(c,types.StringType) or isinstance(c,types.UnicodeType)) or len(c) > 1:
+                    print("lex: Invalid literal %s. Must be a single character" % repr(c))
+                    error = 1
+                    continue
+
+    except TypeError:
+         print("lex: Invalid literals specification. literals must be a sequence of characters.")
+         error = 1
+
+    lexobj.lexliterals = literals
+
+    # Build statemap
+    if states:
+         if not (isinstance(states,types.TupleType) or isinstance(states,types.ListType)):
+              print("lex: states must be defined as a tuple or list.")
+              error = 1
+         else:
+              for s in states:
+                    if not isinstance(s,types.TupleType) or len(s) != 2:
+                           print("lex: invalid state specifier %s. Must be a tuple (statename,'exclusive|inclusive')" % repr(s))
+                           error = 1
+                           continue
+                    name, statetype = s
+                    if not isinstance(name,types.StringType):
+                           print("lex: state name %s must be a string" % repr(name))
+                           error = 1
+                           continue
+                    if not (statetype == 'inclusive' or statetype == 'exclusive'):
+                           print("lex: state type for state %s must be 'inclusive' or 'exclusive'" % name)
+                           error = 1
+                           continue
+                    if stateinfo.has_key(name):
+                           print("lex: state '%s' already defined." % name)
+                           error = 1
+                           continue
+                    stateinfo[name] = statetype
+
+    # Get a list of symbols with the t_ or s_ prefix
+    tsymbols = [f for f in ldict.keys() if f[:2] == 't_' ]
+
+    # Now build up a list of functions and a list of strings
+
+    funcsym =  { }        # Symbols defined as functions
+    strsym =   { }        # Symbols defined as strings
+    toknames = { }        # Mapping of symbols to token names
+
+    for s in stateinfo.keys():
+         funcsym[s] = []
+         strsym[s] = []
+
+    ignore   = { }        # Ignore strings by state
+    errorf   = { }        # Error functions by state
+
+    if len(tsymbols) == 0:
+        raise SyntaxError("lex: no rules of the form t_rulename are defined.")
+
+    for f in tsymbols:
+        t = ldict[f]
+        states, tokname = _statetoken(f,stateinfo)
+        toknames[f] = tokname
+
+        if callable(t):
+            for s in states: funcsym[s].append((f,t))
+        elif (isinstance(t, types.StringType) or isinstance(t,types.UnicodeType)):
+            for s in states: strsym[s].append((f,t))
+        else:
+            print("lex: %s not defined as a function or string" % f)
+            error = 1
+
+    # Sort the functions by line number
+    for f in funcsym.values():
+        f.sort(key=lambda func: func[1].__code__.co_firstlineno)
+
+    # Sort the strings by regular expression length
+    for s in strsym.values():
+        s.sort(lambda x,y: (len(x[1]) < len(y[1])) - (len(x[1]) > len(y[1])))
+
+    regexs = { }
+
+    # Build the master regular expressions
+    for state in stateinfo.keys():
+        regex_list = []
+
+        # Add rules defined by functions first
+        for fname, f in funcsym[state]:
+            line = f.func_code.co_firstlineno
+            file = f.func_code.co_filename
+            files[file] = None
+            tokname = toknames[fname]
+
+            ismethod = isinstance(f, types.MethodType)
+
+            if not optimize:
+                nargs = f.func_code.co_argcount
+                if ismethod:
+                    reqargs = 2
+                else:
+                    reqargs = 1
+                if nargs > reqargs:
+                    print("%s:%d: Rule '%s' has too many arguments." % (file,line,f.__name__))
+                    error = 1
+                    continue
+
+                if nargs < reqargs:
+                    print("%s:%d: Rule '%s' requires an argument." % (file,line,f.__name__))
+                    error = 1
+                    continue
+
+                if tokname == 'ignore':
+                    print("%s:%d: Rule '%s' must be defined as a string." % (file,line,f.__name__))
+                    error = 1
+                    continue
+
+            if tokname == 'error':
+                errorf[state] = f
+                continue
+
+            if f.__doc__:
+                if not optimize:
+                    try:
+                        c = re.compile("(?P<%s>%s)" % (f.__name__,f.__doc__), re.VERBOSE | reflags)
+                        if c.match(""):
+                             print("%s:%d: Regular expression for rule '%s' matches empty string." % (file,line,f.__name__))
+                             error = 1
+                             continue
+                    except re.error as e:
+                        print("%s:%d: Invalid regular expression for rule '%s'. %s" % (file,line,f.__name__,e))
+                        if '#' in f.__doc__:
+                             print("%s:%d. Make sure '#' in rule '%s' is escaped with '\\#'." % (file,line, f.__name__))
+                        error = 1
+                        continue
+
+                    if debug:
+                        print("lex: Adding rule %s -> '%s' (state '%s')" % (f.__name__,f.__doc__, state))
+
+                # Okay. The regular expression seemed okay.  Let's append it to the master regular
+                # expression we're building
+
+                regex_list.append("(?P<%s>%s)" % (f.__name__,f.__doc__))
+            else:
+                print("%s:%d: No regular expression defined for rule '%s'" % (file,line,f.__name__))
+
+        # Now add all of the simple rules
+        for name,r in strsym[state]:
+            tokname = toknames[name]
+
+            if tokname == 'ignore':
+                 ignore[state] = r
+                 continue
+
+            if not optimize:
+                if tokname == 'error':
+                    raise SyntaxError("lex: Rule '%s' must be defined as a function" % name)
+                    error = 1
+                    continue
+
+                if not lexobj.lextokens.has_key(tokname) and tokname.find("ignore_") < 0:
+                    print("lex: Rule '%s' defined for an unspecified token %s." % (name,tokname))
+                    error = 1
+                    continue
+                try:
+                    c = re.compile("(?P<%s>%s)" % (name,r),re.VERBOSE | reflags)
+                    if (c.match("")):
+                         print("lex: Regular expression for rule '%s' matches empty string." % name)
+                         error = 1
+                         continue
+                except re.error as e:
+                    print("lex: Invalid regular expression for rule '%s'. %s" % (name,e))
+                    if '#' in r:
+                         print("lex: Make sure '#' in rule '%s' is escaped with '\\#'." % name)
+
+                    error = 1
+                    continue
+                if debug:
+                    print("lex: Adding rule %s -> '%s' (state '%s')" % (name,r,state))
+
+            regex_list.append("(?P<%s>%s)" % (name,r))
+
+        if not regex_list:
+             print("lex: No rules defined for state '%s'" % state)
+             error = 1
+
+        regexs[state] = regex_list
+
+
+    if not optimize:
+        for f in files.keys():
+           if not _validate_file(f):
+                error = 1
+
+    if error:
+        raise SyntaxError("lex: Unable to build lexer.")
+
+    # From this point forward, we're reasonably confident that we can build the lexer.
+    # No more errors will be generated, but there might be some warning messages.
+
+    # Build the master regular expressions
+
+    for state in regexs.keys():
+        lexre, re_text = _form_master_re(regexs[state],reflags,ldict)
+        lexobj.lexstatere[state] = lexre
+        lexobj.lexstateretext[state] = re_text
+        if debug:
+            for i in range(len(re_text)):
+                 print("lex: state '%s'. regex[%d] = '%s'" % (state, i, re_text[i]))
+
+    # For inclusive states, we need to add the INITIAL state
+    for state,type in stateinfo.items():
+        if state != "INITIAL" and type == 'inclusive':
+             lexobj.lexstatere[state].extend(lexobj.lexstatere['INITIAL'])
+             lexobj.lexstateretext[state].extend(lexobj.lexstateretext['INITIAL'])
+
+    lexobj.lexstateinfo = stateinfo
+    lexobj.lexre = lexobj.lexstatere["INITIAL"]
+    lexobj.lexretext = lexobj.lexstateretext["INITIAL"]
+
+    # Set up ignore variables
+    lexobj.lexstateignore = ignore
+    lexobj.lexignore = lexobj.lexstateignore.get("INITIAL","")
+
+    # Set up error functions
+    lexobj.lexstateerrorf = errorf
+    lexobj.lexerrorf = errorf.get("INITIAL",None)
+    if warn and not lexobj.lexerrorf:
+        print("lex: Warning. no t_error rule is defined.")
+
+    # Check state information for ignore and error rules
+    for s,stype in stateinfo.items():
+        if stype == 'exclusive':
+              if warn and not errorf.has_key(s):
+                   print("lex: Warning. no error rule is defined for exclusive state '%s'" % s)
+              if warn and not ignore.has_key(s) and lexobj.lexignore:
+                   print("lex: Warning. no ignore rule is defined for exclusive state '%s'" % s)
+        elif stype == 'inclusive':
+              if not errorf.has_key(s):
+                   errorf[s] = errorf.get("INITIAL",None)
+              if not ignore.has_key(s):
+                   ignore[s] = ignore.get("INITIAL","")
+
+
+    # Create global versions of the token() and input() functions
+    token = lexobj.token
+    input = lexobj.input
+    lexer = lexobj
+
+    # If in optimize mode, we write the lextab
+    if lextab and optimize:
+        lexobj.writetab(lextab)
+
+    return lexobj
+
+# -----------------------------------------------------------------------------
+# runmain()
+#
+# This runs the lexer as a main program
+# -----------------------------------------------------------------------------
+
+def runmain(lexer=None,data=None):
+    if not data:
+        try:
+            filename = sys.argv[1]
+            f = open(filename)
+            data = f.read()
+            f.close()
+        except IndexError:
+            print("Reading from standard input (type EOF to end):")
+            data = sys.stdin.read()
+
+    if lexer:
+        _input = lexer.input
+    else:
+        _input = input
+    _input(data)
+    if lexer:
+        _token = lexer.token
+    else:
+        _token = token
+
+    while 1:
+        tok = _token()
+        if not tok: break
+        print("(%s,%r,%d,%d)" % (tok.type, tok.value, tok.lineno,tok.lexpos))
+
+
+# -----------------------------------------------------------------------------
+# @TOKEN(regex)
+#
+# This decorator function can be used to set the regex expression on a function
+# when its docstring might need to be set in an alternative way
+# -----------------------------------------------------------------------------
+
+def TOKEN(r):
+    def set_doc(f):
+        f.__doc__ = r
+        return f
+    return set_doc
+
+# Alternative spelling of the TOKEN decorator
+Token = TOKEN
+
diff --git a/tools/wraptypes/preprocessor.py b/tools/wraptypes/preprocessor.py
new file mode 100644
index 0000000..78f26fe
--- /dev/null
+++ b/tools/wraptypes/preprocessor.py
@@ -0,0 +1,1507 @@
+#!/usr/bin/env python
+
+'''Preprocess a C source file.
+
+Limitations:
+
+  * Whitespace is not preserved.
+  * # and ## operators not handled.
+
+Reference is C99:
+  * http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1124.pdf
+  * Also understands Objective-C #import directive
+  * Also understands GNU #include_next
+
+'''
+from __future__ import print_function
+
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+import operator
+import os.path
+import cPickle
+import re
+import sys
+
+import lex
+from lex import TOKEN
+import yacc
+
+tokens = (
+    'HEADER_NAME', 'IDENTIFIER', 'PP_NUMBER', 'CHARACTER_CONSTANT',
+    'STRING_LITERAL', 'OTHER',
+
+    'PTR_OP', 'INC_OP', 'DEC_OP', 'LEFT_OP', 'RIGHT_OP', 'LE_OP', 'GE_OP',
+    'EQ_OP', 'NE_OP', 'AND_OP', 'OR_OP', 'MUL_ASSIGN', 'DIV_ASSIGN',
+    'MOD_ASSIGN', 'ADD_ASSIGN', 'SUB_ASSIGN', 'LEFT_ASSIGN', 'RIGHT_ASSIGN',
+    'AND_ASSIGN', 'XOR_ASSIGN', 'OR_ASSIGN',  'HASH_HASH', 'PERIOD',
+    'ELLIPSIS',
+
+    'IF', 'IFDEF', 'IFNDEF', 'ELIF', 'ELSE', 'ENDIF', 'INCLUDE',
+    'INCLUDE_NEXT', 'DEFINE', 'UNDEF', 'LINE', 'ERROR', 'PRAGMA', 'DEFINED',
+    'IMPORT',
+
+    'NEWLINE', 'LPAREN'
+)
+
+subs = {
+    'D': '[0-9]',
+    'L': '[a-zA-Z_]',
+    'H': '[a-fA-F0-9]',
+    'E': '[Ee][+-]?{D}+',
+    'FS': '[FflL]',
+    'IS': '[uUlL]*',
+}
+# Helper: substitute {foo} with subs[foo] in string (makes regexes more lexy)
+sub_pattern = re.compile('{([^}]*)}')
+def sub_repl_match(m):
+    return subs[m.groups()[0]]
+def sub(s):
+    return sub_pattern.sub(sub_repl_match, s)
+CHARACTER_CONSTANT = sub(r"L?'(\\.|[^\\'])+'")
+STRING_LITERAL = sub(r'L?"(\\.|[^\\"])*"')
+IDENTIFIER = sub('{L}({L}|{D})*')
+
+# --------------------------------------------------------------------------
+# Token value types
+# --------------------------------------------------------------------------
+
+# Numbers represented as int and float types.
+# For all other tokens, type is just str representation.
+
+class StringLiteral(str):
+    def __new__(cls, value):
+        assert value[0] == '"' and value[-1] == '"'
+        # Unescaping probably not perfect but close enough.
+        value = value[1:-1].decode('string_escape')
+        return str.__new__(cls, value)
+
+class SystemHeaderName(str):
+    def __new__(cls, value):
+        assert value[0] == '<' and value[-1] == '>'
+        return str.__new__(cls, value[1:-1])
+
+    def __repr__(self):
+        return '<%s>' % (str(self))
+
+
+# --------------------------------------------------------------------------
+# Token declarations
+# --------------------------------------------------------------------------
+
+punctuators = {
+    # value: (regex, type)
+    r'>>=': (r'>>=', 'RIGHT_ASSIGN'),
+    r'<<=': (r'<<=', 'LEFT_ASSIGN'),
+    r'+=': (r'\+=', 'ADD_ASSIGN'),
+    r'-=': (r'-=', 'SUB_ASSIGN'),
+    r'*=': (r'\*=', 'MUL_ASSIGN'),
+    r'/=': (r'/=', 'DIV_ASSIGN'),
+    r'%=': (r'%=', 'MOD_ASSIGN'),
+    r'&=': (r'&=', 'AND_ASSIGN'),
+    r'^=': (r'\^=', 'XOR_ASSIGN'),
+    r'|=': (r'\|=', 'OR_ASSIGN'),
+    r'>>': (r'>>', 'RIGHT_OP'),
+    r'<<': (r'<<', 'LEFT_OP'),
+    r'++': (r'\+\+', 'INC_OP'),
+    r'--': (r'--', 'DEC_OP'),
+    r'->': (r'->', 'PTR_OP'),
+    r'&&': (r'&&', 'AND_OP'),
+    r'||': (r'\|\|', 'OR_OP'),
+    r'<=': (r'<=', 'LE_OP'),
+    r'>=': (r'>=', 'GE_OP'),
+    r'==': (r'==', 'EQ_OP'),
+    r'!=': (r'!=', 'NE_OP'),
+    r'<:': (r'<:', '['),
+    r':>': (r':>', ']'),
+    r'<%': (r'<%', '{'),
+    r'%>': (r'%>', '}'),
+    r'%:%:': (r'%:%:', 'HASH_HASH'),
+    r';': (r';', ';'),
+    r'{': (r'{', '{'),
+    r'}': (r'}', '}'),
+    r',': (r',', ','),
+    r':': (r':', ':'),
+    r'=': (r'=', '='),
+    r')': (r'\)', ')'),
+    r'[': (r'\[', '['),
+    r']': (r']', ']'),
+    r'.': (r'\.', 'PERIOD'),
+    r'&': (r'&', '&'),
+    r'!': (r'!', '!'),
+    r'~': (r'~', '~'),
+    r'-': (r'-', '-'),
+    r'+': (r'\+', '+'),
+    r'*': (r'\*', '*'),
+    r'/': (r'/', '/'),
+    r'%': (r'%', '%'),
+    r'<': (r'<', '<'),
+    r'>': (r'>', '>'),
+    r'^': (r'\^', '^'),
+    r'|': (r'\|', '|'),
+    r'?': (r'\?', '?'),
+    r'#': (r'\#', '#'),
+}
+
+def punctuator_regex(punctuators):
+    punctuator_regexes = [v[0] for v in punctuators.values()]
+    punctuator_regexes.sort(key=len, reverse=True)
+    return '(%s)' % '|'.join(punctuator_regexes)
+
+def t_clinecomment(t):
+    r'//[^\n]*'
+    t.lexer.lineno += 1
+
+def t_cr(t):
+    r'\r'
+    # Skip over CR characters.  Only necessary on urlopen'd files.
+
+# C /* comments */.  Copied from the ylex.py example in PLY: it's not 100%
+# correct for ANSI C, but close enough for anything that's not crazy.
+def t_ccomment(t):
+    r'/\*(.|\n)*?\*/'
+    t.lexer.lineno += t.value.count('\n')
+
+def t_header_name(t):
+    r'<([\/]?[^\/\*\n>])*[\/]?>(?=[ \t\f\v\r\n])'
+    # Should allow any character from charset, but that wreaks havok (skips
+    #   comment delimiter, for instance), so also don't permit '*' or '//'
+    # The non-matching group at the end prevents false-positives with
+    #   operators like '>='.
+    # In the event of a false positive (e.g. "if (a < b || c > d)"), the
+    #  token will be split and rescanned if it appears in a text production;
+    #  see PreprocessorParser.write.
+    # Is also r'"[^\n"]"', but handled in STRING_LITERAL instead.
+    t.type = 'HEADER_NAME'
+    t.value = SystemHeaderName(t.value)
+    return t
+
+def t_directive(t):
+    r'\#[ \t]*(ifdef|ifndef|if|elif|else|endif|define|undef|include_next|include|import|line|error|pragma)'
+    if t.lexer.lasttoken in ('NEWLINE', None):
+        t.type = t.value[1:].lstrip().upper()
+    else:
+        # TODO
+        t.type = '#'
+        t.lexer.nexttoken = ('IDENTIFIER', t.value[1:].lstrip())
+    return t
+
+@TOKEN(r'(' + IDENTIFIER + r')?\.\.\.')
+def t_ellipsis(t):
+    """In GNU C ellipsis can be prepended with a variable name, so not simply punctuation."""
+    t.type = 'ELLIPSIS'
+    return t
+
+@TOKEN(punctuator_regex(punctuators))
+def t_punctuator(t):
+    t.type = punctuators[t.value][1]
+    return t
+
+@TOKEN(CHARACTER_CONSTANT)
+def t_character_constant(t):
+    t.type = 'CHARACTER_CONSTANT'
+    return t
+
+@TOKEN(IDENTIFIER)
+def t_identifier(t):
+    if t.value == 'defined':
+        t.type = 'DEFINED'
+    else:
+        t.type = 'IDENTIFIER'
+    return t
+
+    # missing: universal-character-constant
+@TOKEN(sub(r'({D}|\.{D})({D}|{L}|e[+-]|E[+-]|p[+-]|P[+-]|\.)*'))
+def t_pp_number(t):
+    t.type = 'PP_NUMBER'
+    return t
+
+@TOKEN(STRING_LITERAL)
+def t_string_literal(t):
+    t.type = 'STRING_LITERAL'
+    t.value = StringLiteral(t.value)
+    return t
+
+def t_lparen(t):
+    r'\('
+    if t.lexpos == 0 or t.lexer.lexdata[t.lexpos-1] not in (' \t\f\v\n'):
+        t.type = 'LPAREN'
+    else:
+        t.type = '('
+    return t
+
+def t_continuation(t):
+    r'\\\n'
+    t.lexer.lineno += 1
+    return None
+
+def t_newline(t):
+    r'\n'
+    t.lexer.lineno += 1
+    t.type = 'NEWLINE'
+    return t
+
+def t_error(t):
+    t.type = 'OTHER'
+    return t
+
+t_ignore = ' \t\v\f'
+
+# --------------------------------------------------------------------------
+# Expression Object Model
+# --------------------------------------------------------------------------
+
+class EvaluationContext(object):
+    '''Interface for evaluating expression nodes.
+    '''
+    def is_defined(self, identifier):
+        return False
+
+class ExpressionNode(object):
+    def evaluate(self, context):
+        return 0
+
+    def __str__(self):
+        return ''
+
+class ConstantExpressionNode(ExpressionNode):
+    def __init__(self, value):
+        self.value = value
+
+    def evaluate(self, context):
+        return self.value
+
+    def __str__(self):
+        return str(self.value)
+
+class UnaryExpressionNode(ExpressionNode):
+    def __init__(self, op, op_str, child):
+        self.op = op
+        self.op_str = op_str
+        self.child = child
+
+    def evaluate(self, context):
+        return self.op(self.child.evaluate(context))
+
+    def __str__(self):
+        return '(%s %s)' % (self.op_str, self.child)
+
+class BinaryExpressionNode(ExpressionNode):
+    def __init__(self, op, op_str, left, right):
+        self.op = op
+        self.op_str = op_str
+        self.left = left
+        self.right = right
+
+    def evaluate(self, context):
+        return self.op(self.left.evaluate(context),
+                       self.right.evaluate(context))
+
+    def __str__(self):
+        return '(%s %s %s)' % (self.left, self.op_str, self.right)
+
+class LogicalAndExpressionNode(ExpressionNode):
+    def __init__(self, left, right):
+        self.left = left
+        self.right = right
+
+    def evaluate(self, context):
+        return self.left.evaluate(context) and self.right.evaluate(context)
+
+    def __str__(self):
+        return '(%s && %s)' % (self.left, self.right)
+
+class LogicalOrExpressionNode(ExpressionNode):
+    def __init__(self, left, right):
+        self.left = left
+        self.right = right
+
+    def evaluate(self, context):
+        return self.left.evaluate(context) or self.right.evaluate(context)
+
+    def __str__(self):
+        return '(%s || %s)' % (self.left, self.right)
+
+class ConditionalExpressionNode(ExpressionNode):
+    def __init__(self, condition, left, right):
+        self.condition = condition
+        self.left = left
+        self.right = right
+
+    def evaluate(self, context):
+        if self.condition.evaluate(context):
+            return self.left.evaluate(context)
+        else:
+            return self.right.evaluate(context)
+
+    def __str__(self):
+        return '(%s ? %s : %s)' % (self.condition, self.left, self.right)
+
+# --------------------------------------------------------------------------
+# Lexers
+# --------------------------------------------------------------------------
+
+class PreprocessorLexer(lex.Lexer):
+    def __init__(self):
+        lex.Lexer.__init__(self)
+        self.filename = '<input>'
+
+    def input(self, data, filename=None):
+        if filename:
+            self.filename = filename
+        self.lasttoken = None
+        self.input_stack = []
+
+        lex.Lexer.input(self, data)
+
+    def push_input(self, data, filename):
+        self.input_stack.append(
+            (self.lexdata, self.lexpos, self.filename, self.lineno))
+        self.lexdata = data
+        self.lexpos = 0
+        self.lineno = 1
+        self.filename = filename
+        self.lexlen = len(self.lexdata)
+
+    def pop_input(self):
+        self.lexdata, self.lexpos, self.filename, self.lineno = \
+            self.input_stack.pop()
+        self.lexlen = len(self.lexdata)
+
+    def token(self):
+        result = lex.Lexer.token(self)
+        while result is None and self.input_stack:
+            self.pop_input()
+            result = lex.Lexer.token(self)
+
+        if result:
+            self.lasttoken = result.type
+            result.filename = self.filename
+        else:
+            self.lasttoken = None
+
+        return result
+
+class TokenListLexer(object):
+    def __init__(self, tokens):
+        self.tokens = tokens
+        self.pos = 0
+
+    def token(self):
+        if self.pos < len(self.tokens):
+            t = self.tokens[self.pos]
+            self.pos += 1
+            return t
+        else:
+            return None
+
+def symbol_to_token(sym):
+    if isinstance(sym, yacc.YaccSymbol):
+        return sym.value
+    elif isinstance(sym, lex.LexToken):
+        return sym
+    else:
+        assert False, 'Not a symbol: %r' % sym
+
+def create_token(type, value, production=None):
+    '''Create a token of type and value, at the position where 'production'
+    was reduced.  Don't specify production if the token is built-in'''
+    t = lex.LexToken()
+    t.type = type
+    t.value = value
+    t.lexpos = -1
+    if production:
+        t.lineno = production.slice[1].lineno
+        t.filename = production.slice[1].filename
+    else:
+        t.lineno = -1
+        t.filename = '<builtin>'
+    return t
+
+# --------------------------------------------------------------------------
+# Grammars
+# --------------------------------------------------------------------------
+
+class Grammar(object):
+    prototype = None
+    name = 'grammar'
+
+    @classmethod
+    def get_prototype(cls):
+        if not cls.prototype:
+            instance = cls()
+            tabmodule = '%stab' % cls.name
+            cls.prototype = yacc.yacc(module=instance, tabmodule=tabmodule)
+        return cls.prototype
+
+class PreprocessorGrammar(Grammar):
+    tokens = tokens
+    name = 'pp'
+
+    def p_preprocessing_file(self, p):
+        '''preprocessing_file : group_opt
+        '''
+
+    def p_group_opt(self, p):
+        '''group_opt : group
+                     |
+        '''
+
+    def p_group(self, p):
+        '''group : group_part
+                 | group group_part
+        '''
+
+    def p_group_part(self, p):
+        '''group_part : if_section
+                      | control_line
+                      | text_line
+        '''
+
+    def p_if_section(self, p):
+        '''if_section : if_group elif_groups_opt else_group_opt endif_line
+        '''
+
+    def p_if_group(self, p):
+        '''if_group : if_line group_opt
+        '''
+
+    def p_if_line(self, p):
+        '''if_line : IF replaced_constant_expression NEWLINE
+                   | IFDEF IDENTIFIER NEWLINE
+                   | IFNDEF IDENTIFIER NEWLINE
+        '''
+        if p.parser.enable_declaratives():
+            type = p.slice[1].type
+            if type == 'IF':
+                if p[2]:
+                    result = p[2].evaluate(p.parser.namespace)
+                else:
+                    # error
+                    result = False
+            elif type == 'IFDEF':
+                result = p.parser.namespace.is_defined(p[2])
+            elif type == 'IFNDEF':
+                result = not p.parser.namespace.is_defined(p[2])
+                p.parser.write((create_token('PP_IFNDEF', p[2], p),))
+        else:
+            result = False
+
+        p.parser.condition_if(result)
+
+    def p_elif_groups_opt(self, p):
+        '''elif_groups_opt : elif_groups
+                           |
+        '''
+
+    def p_elif_groups(self, p):
+        '''elif_groups : elif_group
+                       | elif_groups elif_group
+        '''
+
+    def p_elif_group(self, p):
+        '''elif_group : elif_line group_opt
+        '''
+
+    def p_elif_line(self, p):
+        '''elif_line : ELIF replaced_elif_constant_expression NEWLINE
+        '''
+        result = p[2].evaluate(p.parser.namespace)
+        p.parser.condition_elif(result)
+
+    def p_else_group_opt(self, p):
+        '''else_group_opt : else_group
+                          |
+        '''
+
+    def p_else_group(self, p):
+        '''else_group : else_line group_opt
+        '''
+
+    def p_else_line(self, p):
+        '''else_line : ELSE NEWLINE
+        '''
+        p.parser.condition_else()
+
+    def p_endif_line(self, p):
+        '''endif_line : ENDIF pp_tokens_opt NEWLINE
+        '''
+        # pp_tokens needed (ignored) here for Apple.
+        p.parser.condition_endif()
+
+    def p_control_line(self, p):
+        '''control_line : include_line NEWLINE
+                        | define_object
+                        | define_function
+                        | undef_line
+                        | LINE pp_tokens NEWLINE
+                        | error_line
+                        | PRAGMA pp_tokens_opt NEWLINE
+        '''
+
+    def p_include_line(self, p):
+        '''include_line : INCLUDE pp_tokens
+                        | INCLUDE_NEXT pp_tokens
+                        | IMPORT pp_tokens
+        '''
+        if p.parser.enable_declaratives():
+            tokens = p[2]
+            tokens = p.parser.namespace.apply_macros(tokens)
+            if len(tokens) > 0:
+                if p.slice[1].type == 'INCLUDE':
+                    if tokens[0].type == 'STRING_LITERAL':
+                        p.parser.include(tokens[0].value)
+                        return
+                    elif tokens[0].type == 'HEADER_NAME':
+                        p.parser.include_system(tokens[0].value)
+                        return
+                elif p.slice[1].type == 'INCLUDE_NEXT':
+                    p.parser.include_next(tokens[0].value, p.slice[1].filename)
+                    return
+                else:
+                    if tokens[0].type == 'STRING_LITERAL':
+                        p.parser.import_(tokens[0].value)
+                        return
+                    elif tokens[0].type == 'HEADER_NAME':
+                        p.parser.import_system(tokens[0].value)
+                        return
+
+            # TODO
+            print('Invalid #include', file=sys.stderr)
+
+    def p_define_object(self, p):
+        '''define_object : DEFINE IDENTIFIER replacement_list NEWLINE
+        '''
+        if p.parser.enable_declaratives():
+            p.parser.namespace.define_object(p[2], p[3])
+
+            # Try to parse replacement list as an expression
+            tokens = p.parser.namespace.apply_macros(p[3])
+            lexer = TokenListLexer(tokens)
+            expr_parser = StrictConstantExpressionParser(lexer,
+                                                         p.parser.namespace)
+            value = expr_parser.parse(debug=False)
+            if value is not None:
+                value = value.evaluate(p.parser.namespace)
+                p.parser.write(
+                    (create_token('PP_DEFINE_CONSTANT', (p[2], value), p),))
+            else:
+                # Didn't parse, pass on as string
+                value = ' '.join([str(t.value) for t in p[3]])
+                p.parser.write((create_token('PP_DEFINE', (p[2], value), p),))
+
+    def p_define_function(self, p):
+        '''define_function : DEFINE IDENTIFIER LPAREN define_function_params ')' pp_tokens_opt NEWLINE
+        '''
+        if p.parser.enable_declaratives():
+            p.parser.namespace.define_function(p[2], p[4], p[6])
+
+    def p_define_function_params(self, p):
+        '''define_function_params : identifier_list_opt
+                                  | ELLIPSIS
+                                  | identifier_list ',' ELLIPSIS
+        '''
+        if len(p) == 2:
+            if p[1] == 'ELLIPSIS':
+                p[0] = ('...',)
+            else:
+                p[0] = p[1]
+        else:
+            p[0] = p[1] + ('...',)
+
+    def p_undef_line(self, p):
+        '''undef_line : UNDEF IDENTIFIER NEWLINE
+        '''
+        if p.parser.enable_declaratives():
+            p.parser.namespace.undef(p[2])
+
+    def p_error_line(self, p):
+        '''error_line : ERROR pp_tokens_opt NEWLINE
+        '''
+        if p.parser.enable_declaratives():
+            p.parser.error(' '.join([t.value for t in p[2]]),
+                           p.slice[1].filename, p.slice[1].lineno)
+
+    def p_text_line(self, p):
+        '''text_line : pp_tokens_opt NEWLINE
+        '''
+        if p.parser.enable_declaratives():
+            tokens = p[1]
+            tokens = p.parser.namespace.apply_macros(tokens)
+            p.parser.write(tokens)
+
+    def p_replacement_list(self, p):
+        '''replacement_list :
+                            | preprocessing_token_no_lparen
+                            | preprocessing_token_no_lparen pp_tokens
+        '''
+        if len(p) == 3:
+            p[0] = (p[1],) + p[2]
+        elif len(p) == 2:
+            p[0] = (p[1],)
+        else:
+            p[0] = ()
+
+    def p_identifier_list_opt(self, p):
+        '''identifier_list_opt : identifier_list
+                               |
+        '''
+        if len(p) == 2:
+            p[0] = p[1]
+        else:
+            p[0] = ()
+
+    def p_identifier_list(self, p):
+        '''identifier_list : IDENTIFIER
+                           | identifier_list ',' IDENTIFIER
+        '''
+        if len(p) > 2:
+            p[0] = p[1] + (p[3],)
+        else:
+            p[0] = (p[1],)
+
+    def p_replaced_constant_expression(self, p):
+        '''replaced_constant_expression : pp_tokens'''
+        if p.parser.enable_conditionals():
+            tokens = p[1]
+            tokens = p.parser.namespace.apply_macros(tokens)
+            lexer = TokenListLexer(tokens)
+            parser = ConstantExpressionParser(lexer, p.parser.namespace)
+            p[0] = parser.parse(debug=True)
+        else:
+            p[0] = ConstantExpressionNode(0)
+
+    def p_replaced_elif_constant_expression(self, p):
+        '''replaced_elif_constant_expression : pp_tokens'''
+        if p.parser.enable_elif_conditionals():
+            tokens = p[1]
+            tokens = p.parser.namespace.apply_macros(tokens)
+            lexer = TokenListLexer(tokens)
+            parser = ConstantExpressionParser(lexer, p.parser.namespace)
+            p[0] = parser.parse(debug=True)
+        else:
+            p[0] = ConstantExpressionNode(0)
+
+
+    def p_pp_tokens_opt(self, p):
+        '''pp_tokens_opt : pp_tokens
+                         |
+        '''
+        if len(p) == 2:
+            p[0] = p[1]
+        else:
+            p[0] = ()
+
+    def p_pp_tokens(self, p):
+        '''pp_tokens : preprocessing_token
+                     | pp_tokens preprocessing_token
+        '''
+        if len(p) == 2:
+            p[0] = (p[1],)
+        else:
+            p[0] = p[1] + (p[2],)
+
+    def p_preprocessing_token_no_lparen(self, p):
+        '''preprocessing_token_no_lparen : HEADER_NAME
+                                         | IDENTIFIER
+                                         | PP_NUMBER
+                                         | CHARACTER_CONSTANT
+                                         | STRING_LITERAL
+                                         | punctuator
+                                         | DEFINED
+                                         | OTHER
+        '''
+        p[0] = symbol_to_token(p.slice[1])
+
+    def p_preprocessing_token(self, p):
+        '''preprocessing_token : preprocessing_token_no_lparen
+                               | LPAREN
+        '''
+        p[0] = symbol_to_token(p.slice[1])
+
+    def p_punctuator(self, p):
+        '''punctuator : ELLIPSIS
+                      | RIGHT_ASSIGN
+                      | LEFT_ASSIGN
+                      | ADD_ASSIGN
+                      | SUB_ASSIGN
+                      | MUL_ASSIGN
+                      | DIV_ASSIGN
+                      | MOD_ASSIGN
+                      | AND_ASSIGN
+                      | XOR_ASSIGN
+                      | OR_ASSIGN
+                      | RIGHT_OP
+                      | LEFT_OP
+                      | INC_OP
+                      | DEC_OP
+                      | PTR_OP
+                      | AND_OP
+                      | OR_OP
+                      | LE_OP
+                      | GE_OP
+                      | EQ_OP
+                      | NE_OP
+                      | HASH_HASH
+                      | ';'
+                      | '{'
+                      | '}'
+                      | ','
+                      | ':'
+                      | '='
+                      | '('
+                      | ')'
+                      | '['
+                      | ']'
+                      | PERIOD
+                      | '&'
+                      | '!'
+                      | '~'
+                      | '-'
+                      | '+'
+                      | '*'
+                      | '/'
+                      | '%'
+                      | '<'
+                      | '>'
+                      | '^'
+                      | '|'
+                      | '?'
+                      | '#'
+        '''
+        p[0] = symbol_to_token(p.slice[1])
+
+    def p_error(self, t):
+        if not t:
+            # Crap, no way to get to Parser instance.  FIXME TODO
+            print('Syntax error at end of file.', file=sys.stderr)
+        else:
+            # TODO
+            print('%s:%d Syntax error at %r' % \
+                (t.lexer.filename, t.lexer.lineno, t.value), file=sys.stderr)
+            #t.lexer.cparser.handle_error('Syntax error at %r' % t.value,
+            #     t.lexer.filename, t.lexer.lineno)
+        # Don't alter lexer: default behaviour is to pass error production
+        # up until it hits the catch-all at declaration, at which point
+        # parsing continues (synchronisation).
+
+class ConstantExpressionParseException(Exception):
+    pass
+
+class ConstantExpressionGrammar(Grammar):
+    name = 'expr'
+    tokens = tokens
+
+    def p_constant_expression(self, p):
+        '''constant_expression : conditional_expression
+        '''
+        p[0] = p[1]
+        p.parser.result = p[0]
+
+    def p_character_constant(self, p):
+        '''character_constant : CHARACTER_CONSTANT
+        '''
+        try:
+            value = ord(eval(p[1].lstrip('L')))
+        except Exception:
+            value = 0
+        p[0] = ConstantExpressionNode(value)
+
+    def p_constant(self, p):
+        '''constant : PP_NUMBER
+        '''
+        value = p[1].rstrip('LlUu')
+        try:
+            if value[:2] == '0x':
+                value = int(value[2:], 16)
+            elif value[0] == '0':
+                value = int(value, 8)
+            else:
+                value = int(value)
+        except ValueError:
+            value = value.rstrip('eEfF')
+            try:
+                value = float(value)
+            except ValueError:
+                value = 0
+        p[0] = ConstantExpressionNode(value)
+
+    def p_identifier(self, p):
+        '''identifier : IDENTIFIER
+        '''
+        p[0] = ConstantExpressionNode(0)
+
+    def p_primary_expression(self, p):
+        '''primary_expression : constant
+                              | character_constant
+                              | identifier
+                              | '(' expression ')'
+                              | LPAREN expression ')'
+        '''
+        if p[1] == '(':
+            p[0] = p[2]
+        else:
+            p[0] = p[1]
+
+    def p_postfix_expression(self, p):
+        '''postfix_expression : primary_expression
+        '''
+        p[0] = p[1]
+
+    def p_unary_expression(self, p):
+        '''unary_expression : postfix_expression
+                            | unary_operator cast_expression
+        '''
+        if len(p) == 2:
+            p[0] = p[1]
+        elif type(p[1]) == tuple:
+            # unary_operator reduces to (op, op_str)
+            p[0] = UnaryExpressionNode(p[1][0], p[1][1], p[2])
+        else:
+            # TODO
+            p[0] = None
+
+    def p_unary_operator(self, p):
+        '''unary_operator : '+'
+                          | '-'
+                          | '~'
+                          | '!'
+        '''
+        # reduces to (op, op_str)
+        p[0] = ({
+            '+': operator.pos,
+            '-': operator.neg,
+            '~': operator.inv,
+            '!': operator.not_}[p[1]], p[1])
+
+    def p_cast_expression(self, p):
+        '''cast_expression : unary_expression
+        '''
+        p[0] = p[len(p) - 1]
+
+    def p_multiplicative_expression(self, p):
+        '''multiplicative_expression : cast_expression
+                             | multiplicative_expression '*' cast_expression
+                             | multiplicative_expression '/' cast_expression
+                             | multiplicative_expression '%' cast_expression
+        '''
+        if len(p) == 2:
+            p[0] = p[1]
+        else:
+            p[0] = BinaryExpressionNode({
+                '*': operator.mul,
+                '/': operator.div,
+                '%': operator.mod}[p[2]], p[2], p[1], p[3])
+
+    def p_additive_expression(self, p):
+        '''additive_expression : multiplicative_expression
+                       | additive_expression '+' multiplicative_expression
+                       | additive_expression '-' multiplicative_expression
+        '''
+        if len(p) == 2:
+            p[0] = p[1]
+        else:
+            p[0] = BinaryExpressionNode({
+                '+': operator.add,
+                '-': operator.sub}[p[2]], p[2], p[1], p[3])
+
+    def p_shift_expression(self, p):
+        '''shift_expression : additive_expression
+                            | shift_expression LEFT_OP additive_expression
+                            | shift_expression RIGHT_OP additive_expression
+        '''
+        if len(p) == 2:
+            p[0] = p[1]
+        else:
+            p[0] = BinaryExpressionNode({
+                '<<': operator.lshift,
+                '>>': operator.rshift}[p[2]], p[2], p[1], p[3])
+
+    def p_relational_expression(self, p):
+        '''relational_expression : shift_expression
+                                 | relational_expression '<' shift_expression
+                                 | relational_expression '>' shift_expression
+                                 | relational_expression LE_OP shift_expression
+                                 | relational_expression GE_OP shift_expression
+        '''
+        if len(p) == 2:
+            p[0] = p[1]
+        else:
+            p[0] = BinaryExpressionNode({
+                '>': operator.gt,
+                '<': operator.lt,
+                '<=': operator.le,
+                '>=': operator.ge}[p[2]], p[2], p[1], p[3])
+
+    def p_equality_expression(self, p):
+        '''equality_expression : relational_expression
+                               | equality_expression EQ_OP relational_expression
+                               | equality_expression NE_OP relational_expression
+        '''
+        if len(p) == 2:
+            p[0] = p[1]
+        else:
+            p[0] = BinaryExpressionNode({
+                '==': operator.eq,
+                '!=': operator.ne}[p[2]], p[2], p[1], p[3])
+
+    def p_and_expression(self, p):
+        '''and_expression : equality_expression
+                          | and_expression '&' equality_expression
+        '''
+        if len(p) == 2:
+            p[0] = p[1]
+        else:
+            p[0] = BinaryExpressionNode(operator.and_, '&', p[1], p[3])
+
+    def p_exclusive_or_expression(self, p):
+        '''exclusive_or_expression : and_expression
+                                   | exclusive_or_expression '^' and_expression
+        '''
+        if len(p) == 2:
+            p[0] = p[1]
+        else:
+            p[0] = BinaryExpressionNode(operator.xor, '^', p[1], p[3])
+
+    def p_inclusive_or_expression(self, p):
+        '''inclusive_or_expression : exclusive_or_expression
+                       | inclusive_or_expression '|' exclusive_or_expression
+        '''
+        if len(p) == 2:
+            p[0] = p[1]
+        else:
+            p[0] = BinaryExpressionNode(operator.or_, '|', p[1], p[3])
+
+    def p_logical_and_expression(self, p):
+        '''logical_and_expression : inclusive_or_expression
+                      | logical_and_expression AND_OP inclusive_or_expression
+        '''
+        if len(p) == 2:
+            p[0] = p[1]
+        else:
+            p[0] = LogicalAndExpressionNode(p[1], p[3])
+
+    def p_logical_or_expression(self, p):
+        '''logical_or_expression : logical_and_expression
+                      | logical_or_expression OR_OP logical_and_expression
+        '''
+        if len(p) == 2:
+            p[0] = p[1]
+        else:
+            p[0] = LogicalOrExpressionNode(p[1], p[3])
+
+
+    def p_conditional_expression(self, p):
+        '''conditional_expression : logical_or_expression
+              | logical_or_expression '?' expression ':' conditional_expression
+        '''
+        if len(p) == 2:
+            p[0] = p[1]
+        else:
+            p[0] = ConditionalExpressionNode(p[1], p[3], p[5])
+
+    def p_assignment_expression(self, p):
+        '''assignment_expression : conditional_expression
+                 | unary_expression assignment_operator assignment_expression
+        '''
+        # TODO assignment
+        if len(p) == 2:
+            p[0] = p[1]
+
+    def p_assignment_operator(self, p):
+        '''assignment_operator : '='
+                               | MUL_ASSIGN
+                               | DIV_ASSIGN
+                               | MOD_ASSIGN
+                               | ADD_ASSIGN
+                               | SUB_ASSIGN
+                               | LEFT_ASSIGN
+                               | RIGHT_ASSIGN
+                               | AND_ASSIGN
+                               | XOR_ASSIGN
+                               | OR_ASSIGN
+        '''
+
+    def p_expression(self, p):
+        '''expression : assignment_expression
+                      | expression ',' assignment_expression
+        '''
+        # TODO sequence
+        if len(p) == 2:
+            p[0] = p[1]
+
+    def p_error(self, t):
+        raise ConstantExpressionParseException()
+
+class StrictConstantExpressionGrammar(ConstantExpressionGrammar):
+    name = 'strict_expr'
+    tokens = tokens
+
+    def p_identifier(self, p):
+        '''identifier : IDENTIFIER
+        '''
+        raise ConstantExpressionParseException()
+
+class ExecutionState(object):
+    def __init__(self, parent_enabled, enabled):
+        self.enabled = parent_enabled and enabled
+        self.context_enabled = enabled
+        self.parent_enabled = parent_enabled
+
+    def enable(self, result):
+        if result:
+            self.enabled = self.parent_enabled and not self.context_enabled
+            self.context_enabled = True
+        else:
+            self.enabled = False
+
+class PreprocessorParser(yacc.Parser):
+    def __init__(self, gcc_search_path=True):
+        yacc.Parser.__init__(self)
+        self.lexer = lex.lex(cls=PreprocessorLexer)
+        PreprocessorGrammar.get_prototype().init_parser(self)
+
+        # Map system header name to data, overrides path search and open()
+        self.system_headers = {}
+
+
+        self.include_path = ['/usr/local/include', '/usr/include']
+        if sys.platform == 'darwin':
+            self.framework_path = ['/System/Library/Frameworks',
+                                   '/Library/Frameworks']
+        else:
+            self.framework_path = []
+
+        if gcc_search_path:
+            self.add_gcc_search_path()
+            self.add_cpp_search_path()
+
+        self.lexer.filename = ''
+
+        self.defines = {}
+        self.namespace = PreprocessorNamespace()
+
+    def define(self, name, value):
+        self.defines[name] = value
+
+    def add_gcc_search_path(self):
+        from subprocess import Popen, PIPE
+        path = Popen('gcc -print-file-name=include',
+                     shell=True, stdout=PIPE).communicate()[0].strip()
+        if path:
+            self.include_path.append(path)
+
+    def add_cpp_search_path(self):
+        from subprocess import Popen, PIPE
+        try:
+            open('test.h', 'a').close()
+            output = Popen('cpp -v test.h', shell=True, stderr=PIPE).communicate()[1]
+        finally:
+            os.remove('test.h')
+        if output:
+            output = output.split('\n')
+            while output and not '#include <...>' in output[0]:
+                print(('Skipping:', output[0]))
+                del output[0]
+            if output:
+                del output[0]  # Remove start line
+                while output and not 'End of search list' in output[0]:
+                    self.include_path.append(output[0].strip())
+                    print(('Adding:', output[0].strip()))
+                    del output[0]
+
+    def parse(self, filename=None, data=None, namespace=None, debug=False):
+        self.output = []
+        if not namespace:
+            namespace = self.namespace
+        for name, value in self.defines.items():
+            namespace.define_object(name, (create_token('IDENTIFIER', value),))
+        self.namespace = namespace
+        self.imported_headers = set()
+        self.condition_stack = [ExecutionState(True, True)]
+        if filename:
+            if not data:
+                data = open(filename, 'r').read()
+            self.lexer.input(data, filename)
+        elif data:
+            self.lexer.input(data, '<input>')
+
+        return yacc.Parser.parse(self, debug=debug)
+
+    def push_file(self, filename, data=None):
+        print(filename, file=sys.stderr)
+        if not data:
+            data = open(filename).read()
+        self.lexer.push_input(data, filename)
+
+    def include(self, header):
+        path = self.get_header_path(header)
+        if path:
+            self.push_file(path)
+        else:
+            print('"%s" not found' % header, file=sys.stderr) # TODO
+
+    def include_system(self, header):
+        if header in self.system_headers:
+            self.push_file(header, self.system_headers[header])
+            return
+
+        path = self.get_system_header_path(header)
+        if path:
+            self.push_file(path)
+        else:
+            print('"%s" not found' % header, file=sys.stderr) # TODO
+
+    def include_next(self, header, reference):
+        # XXX doesn't go via get_system_header
+        next = False
+        for path in self.include_path:
+            p = os.path.join(path, header)
+            if os.path.exists(p):
+                if next:
+                    self.push_file(p)
+                    return
+                elif p == reference:
+                    next = True
+        print('%s: cannot include_next from %s' % \
+            (header, reference), file=sys.stderr) # TODO
+
+    def import_(self, header):
+        path = self.get_header_path(header)
+        if path:
+            if path not in self.imported_headers:
+                self.imported_headers.add(path)
+                self.push_file(path)
+        else:
+            print('"%s" not found' % header, file=sys.stderr) # TODO
+
+    def import_system(self, header):
+        if header in self.system_headers:
+            if path not in self.imported_headers:
+                self.imported_headers.add(path)
+                self.push_file(header, self.system_headers[header])
+            return
+        path = self.get_system_header_path(header)
+        if path:
+            if path not in self.imported_headers:
+                self.imported_headers.add(path)
+                self.push_file(path)
+        else:
+            print('"%s" not found' % header, file=sys.stderr) # TODO
+
+    def get_header_path(self, header):
+        p = os.path.join(os.path.dirname(self.lexer.filename), header)
+        if os.path.exists(p):
+            self.push_file(p)
+            return p
+        elif sys.platform == 'darwin':
+            p = self.get_framework_header_path(header)
+            if not p:
+                p = self.get_system_header_path(header)
+            return p
+
+    def get_system_header_path(self, header):
+        for path in self.include_path:
+            p = os.path.join(path, header)
+            if os.path.exists(p):
+                return p
+        if sys.platform == 'darwin':
+            return self.get_framework_header_path(header)
+
+    def get_framework_header_path(self, header):
+        if '/' in header:
+            # header is 'Framework/Framework.h' (e.g. OpenGL/OpenGL.h).
+            framework, header = header.split('/', 1)
+
+            paths = self.framework_path[:]
+            # Add ancestor frameworks of current file
+            localpath = ''
+            for parent in self.lexer.filename.split('.framework/')[:-1]:
+                localpath += parent + '.framework'
+                paths.append(os.path.join(localpath, 'Frameworks'))
+            for path in paths:
+                p = os.path.join(path, '%s.framework' % framework,
+                                 'Headers', header)
+                if os.path.exists(p):
+                    return p
+
+    def error(self, message, filename, line):
+        print('%s:%d #error %s' % (filename, line, message), file=sys.stderr)
+
+    def condition_if(self, result):
+        self.condition_stack.append(
+            ExecutionState(self.condition_stack[-1].enabled, result))
+
+    def condition_elif(self, result):
+        self.condition_stack[-1].enable(result)
+
+    def condition_else(self):
+        self.condition_stack[-1].enable(True)
+
+    def condition_endif(self):
+        self.condition_stack.pop()
+
+    def enable_declaratives(self):
+        return self.condition_stack[-1].enabled
+
+    def enable_conditionals(self):
+        return self.condition_stack[-1].enabled
+
+    def enable_elif_conditionals(self):
+        return self.condition_stack[-1].parent_enabled and \
+               not self.condition_stack[-1].context_enabled
+
+    def write(self, tokens):
+        for t in tokens:
+            if t.type == 'HEADER_NAME':
+                # token was mis-parsed.  Do it again, without the '<', '>'.
+                ta = create_token('<', '<')
+                ta.filename = t.filename
+                ta.lineno = t.lineno
+                self.output.append(ta)
+
+                l = lex.lex(cls=PreprocessorLexer)
+                l.input(t.value, t.filename)
+                l.lineno = t.lineno
+                tb = l.token()
+                while tb is not None:
+                    if hasattr(tb, 'lexer'):
+                        del tb.lexer
+                    self.output.append(tb)
+                    tb = l.token()
+
+                tc = create_token('>', '>')
+                tc.filename = t.filename
+                tc.lineno = t.lineno
+                self.output.append(tc)
+
+                continue
+
+            if hasattr(t, 'lexer'):
+                del t.lexer
+            self.output.append(t)
+
+    def get_memento(self):
+        return (set(self.namespace.objects.keys()),
+                set(self.namespace.functions.keys()))
+
+class ConstantExpressionParser(yacc.Parser):
+    _const_grammar = ConstantExpressionGrammar
+
+    def __init__(self, lexer, namespace):
+        yacc.Parser.__init__(self)
+        self.lexer = lexer
+        self.namespace = namespace
+        self._const_grammar.get_prototype().init_parser(self)
+
+    def parse(self, debug=False):
+        self.result = None
+        try:
+            yacc.Parser.parse(self, lexer=self.lexer, debug=debug)
+        except ConstantExpressionParseException:
+            # XXX warning here?
+            pass
+        return self.result
+
+class StrictConstantExpressionParser(ConstantExpressionParser):
+    _const_grammar = StrictConstantExpressionGrammar
+
+class PreprocessorNamespace(EvaluationContext):
+    def __init__(self, gcc_macros=True,
+                       stdc_macros=True,
+                       workaround_macros=True):
+        self.objects = {}
+        self.functions = {}
+
+        if stdc_macros:
+            self.add_stdc_macros()
+
+        if gcc_macros:
+            self.add_gcc_macros()
+
+        if workaround_macros:
+            self.add_workaround_macros()
+
+    def add_stdc_macros(self):
+        '''Add macros defined in 6.10.8 except __FILE__ and __LINE__.
+
+        This is potentially dangerous, as this preprocessor is not ISO
+        compliant in many ways (the most obvious is the lack of # and ##
+        operators).  It is required for Apple headers, however, which
+        otherwise assume some truly bizarre syntax is ok.
+        '''
+        import time
+        date = time.strftime('%b %d %Y') # XXX %d should have leading space
+        t = time.strftime('%H:%M:S')
+        self.define_object('__DATE__',
+                           (create_token('STRING_LITERAL', date),))
+        self.define_object('__TIME__',
+                           (create_token('STRING_LITERAL', t),))
+        self.define_object('__STDC__',
+                           (create_token('PP_NUMBER', '1'),))
+        self.define_object('__STDC_HOSTED__',
+                           (create_token('PP_NUMBER', '1'),))
+        self.define_object('__STDC_VERSION',
+                           (create_token('PP_NUMBER', '199901L'),))
+
+    def add_gcc_macros(self):
+        import platform
+        import sys
+
+        gcc_macros = ('__GLIBC_HAVE_LONG_LONG', '__GNUC__',)
+
+        # Get these from `gcc -E -dD empty.c`
+        machine_macros = {
+            'x86_64': ('__amd64', '__amd64__', '__x86_64', '__x86_64__',
+                       '__tune_k8__', '__MMX__', '__SSE__', '__SSE2__',
+                       '__SSE_MATH__', '__k8', '__k8__'),
+            'Power Macintosh': ('_ARCH_PPC', '__BIG_ENDIAN__', '_BIG_ENDIAN',
+                                '__ppc__', '__POWERPC__'),
+            # TODO everyone else.
+        }.get(platform.machine(), ())
+        platform_macros = {
+            'linux': ('__gnu_linux__', '__linux', '__linux__', 'linux',
+                       '__unix', '__unix__', 'unix'),
+            'linux2': ('__gnu_linux__', '__linux', '__linux__', 'linux',
+                       '__unix', '__unix__', 'unix'),
+            'linux3': ('__gnu_linux__', '__linux', '__linux__', 'linux',
+                       '__unix', '__unix__', 'unix'),
+            'darwin': ('__MACH__', '__APPLE__', '__DYNAMIC__', '__APPLE_CC__'),
+            'win32':  ('_WIN32',),
+            # TODO everyone else
+        }.get(sys.platform, ())
+
+        tok1 = lex.LexToken()
+        tok1.type = 'PP_NUMBER'
+        tok1.value = '1'
+        tok1.lineno = -1
+        tok1.lexpos = -1
+
+        for macro in machine_macros + platform_macros + gcc_macros:
+            self.define_object(macro, (tok1,))
+
+        self.define_object('inline', ())
+        self.define_object('__inline', ())
+        self.define_object('__inline__', ())
+        self.define_object('__const', (create_token('IDENTIFIER', 'const'),))
+
+    def add_workaround_macros(self):
+        if sys.platform == 'darwin':
+            self.define_object('CF_INLINE', ())
+
+    def is_defined(self, name):
+        return name in self.objects or name in self.functions
+
+    def undef(self, name):
+        if name in self.objects:
+            del self.objects[name]
+
+        if name in self.functions:
+            del self.functions[name]
+
+    def define_object(self, name, replacements):
+        # TODO check not already existing in objects or functions
+        for r in replacements:
+            if hasattr(r, 'lexer'):
+                del r.lexer
+        self.objects[name] = replacements
+
+    def define_function(self, name, params, replacements):
+        # TODO check not already existing in objects or functions
+        for r in replacements:
+            if hasattr(r, 'lexer'):
+                del r.lexer
+        replacements = list(replacements)
+        params = list(params)
+        numargs = len(params)
+        for i, t in enumerate(replacements):
+            if hasattr(t, 'lexer'):
+                del t.lexer
+            if t.type == 'IDENTIFIER' and t.value in params:
+                replacements[i] = params.index(t.value)
+            elif t.type == 'IDENTIFIER' and t.value == '__VA_ARGS__' and \
+                '...' in params:
+                replacements[i] = len(params) - 1
+
+        self.functions[name] = replacements, numargs
+
+    def apply_macros(self, tokens, replacing=None):
+        repl = []
+        i = 0
+        while i < len(tokens):
+            token = tokens[i]
+            if token.type == 'IDENTIFIER' and token.value in self.objects:
+                r = self.objects[token.value]
+                if token.value != replacing and r:
+                    repl += self.apply_macros(r, token.value)
+            elif token.type == 'IDENTIFIER' and \
+                 token.value in self.functions and \
+                 len(tokens) - i > 2 and \
+                 tokens[i+1].value == '(':
+
+                r, numargs = self.functions[token.value][:]
+
+                # build params list
+                i += 2
+                params = [[]]
+                parens = 0  # balance parantheses within each arg
+                while i < len(tokens):
+                    if tokens[i].value == ',' and parens == 0 and \
+                       len(params) < numargs:
+                        params.append([])
+                    elif tokens[i].value == ')' and parens == 0:
+                        break
+                    else:
+                        if tokens[i].value == '(':
+                            parens += 1
+                        elif tokens[i].value == ')':
+                            parens -= 1
+                        params[-1].append(tokens[i])
+                    i += 1
+
+                if token.value != replacing and r:
+                    newr = []
+                    for t in r:
+                        if type(t) == int:
+                            newr += params[t]
+                        else:
+                            newr.append(t)
+                    repl += self.apply_macros(newr, token.value)
+            elif token.type == 'DEFINED':
+                if len(tokens) - i > 3 and \
+                   tokens[i + 1].type in ('(', 'LPAREN') and \
+                   tokens[i + 2].type == 'IDENTIFIER' and \
+                   tokens[i + 3].type == ')':
+                    result = self.is_defined(tokens[i + 2].value)
+                    i += 3
+                elif len(tokens) - i > 1 and \
+                    tokens[i + 1].type == 'IDENTIFIER':
+                    result = self.is_defined(tokens[i + 1].value)
+                    i += 1
+                else:
+                    # TODO
+                    print('Invalid use of "defined"', file=sys.stderr)
+                    result = 0
+                t = lex.LexToken()
+                t.value = str(int(result))
+                t.type = 'PP_NUMBER'
+                t.lexpos = token.lexpos
+                t.lineno = token.lineno
+                repl.append(t)
+            else:
+                repl.append(token)
+            i += 1
+        return repl
+
+    def copy(self):
+        n = PreprocessorNamespace(gcc_macros=False, workaround_macros=False)
+        n.functions = self.functions.copy()
+        n.objects = self.objects.copy()
+        return n
+
+if __name__ == '__main__':
+    filename = sys.argv[1]
+    parser = PreprocessorParser()
+    parser.parse(filename, debug=True)
+    print(' '.join([str(t.value) for t in parser.output]))
diff --git a/tools/wraptypes/wrap.py b/tools/wraptypes/wrap.py
new file mode 100644
index 0000000..890a241
--- /dev/null
+++ b/tools/wraptypes/wrap.py
@@ -0,0 +1,264 @@
+#!/usr/bin/env python
+
+'''Generate a Python ctypes wrapper file for a header file.
+
+Usage example::
+    wrap.py -lGL -oGL.py /usr/include/GL/gl.h
+
+    >>> from GL import *
+
+'''
+from __future__ import print_function
+
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+from ctypesparser import *
+import textwrap
+import sys
+
+class CtypesWrapper(CtypesParser, CtypesTypeVisitor):
+    file=None
+    def begin_output(self, output_file, library, link_modules=(), 
+                     emit_filenames=(), all_headers=False):
+        self.library = library
+        self.file = output_file
+        self.all_names = []
+        self.known_types = {}
+        self.structs = set()
+        self.enums = set()
+        self.emit_filenames = emit_filenames
+        self.all_headers = all_headers
+
+        self.linked_symbols = {}
+        for name in link_modules:
+            module = __import__(name, globals(), locals(), ['foo'])
+            for symbol in dir(module):
+                if symbol not in self.linked_symbols:
+                    self.linked_symbols[symbol] = '%s.%s' % (name, symbol)
+        self.link_modules = link_modules
+
+        self.print_preamble()
+        self.print_link_modules_imports()
+
+    def wrap(self, filename, source=None):
+        assert self.file, 'Call begin_output first'
+        self.parse(filename, source)
+
+    def end_output(self):
+        self.print_epilogue()
+        self.file = None
+
+    def does_emit(self, symbol, filename):
+        return self.all_headers or filename in self.emit_filenames
+
+    def print_preamble(self):
+        import textwrap
+        import time
+        print(textwrap.dedent("""
+            '''Wrapper for %(library)s
+            
+            Generated with:
+            %(argv)s
+            
+            Do not modify this file.
+            '''
+
+            __docformat__ =  'restructuredtext'
+            __version__ = '$Id$'
+
+            import ctypes
+            from ctypes import *
+
+            import pyglet.lib
+
+            _lib = pyglet.lib.load_library(%(library)r)
+
+            _int_types = (c_int16, c_int32)
+            if hasattr(ctypes, 'c_int64'):
+                # Some builds of ctypes apparently do not have c_int64
+                # defined; it's a pretty good bet that these builds do not
+                # have 64-bit pointers.
+                _int_types += (ctypes.c_int64,)
+            for t in _int_types:
+                if sizeof(t) == sizeof(c_size_t):
+                    c_ptrdiff_t = t
+
+            class c_void(Structure):
+                # c_void_p is a buggy return type, converting to int, so
+                # POINTER(None) == c_void_p is actually written as
+                # POINTER(c_void), so it can be treated as a real pointer.
+                _fields_ = [('dummy', c_int)]
+
+        """ % {
+            'library': self.library,
+            'date': time.ctime(),
+            'class': self.__class__.__name__,
+            'argv': ' '.join(sys.argv),
+        }).lstrip(), file=self.file)
+
+    def print_link_modules_imports(self):
+        for name in self.link_modules:
+            print('import %s' % name, file=self.file)
+        print(file=self.file)
+
+    def print_epilogue(self):
+        print(file=self.file)
+        print('\n'.join(textwrap.wrap(
+            '__all__ = [%s]' % ', '.join([repr(n) for n in self.all_names]),
+            width=78,
+            break_long_words=False)), file=self.file)
+
+    def handle_ctypes_constant(self, name, value, filename, lineno):
+        if self.does_emit(name, filename):
+            print('%s = %r' % (name, value), end=' ', file=self.file)
+            print('\t# %s:%d' % (filename, lineno), file=self.file)
+            self.all_names.append(name)
+
+    def handle_ctypes_type_definition(self, name, ctype, filename, lineno):
+        if self.does_emit(name, filename):
+            self.all_names.append(name)
+            if name in self.linked_symbols:
+                print('%s = %s' % \
+                    (name, self.linked_symbols[name]), file=self.file)
+            else:
+                ctype.visit(self)
+                self.emit_type(ctype)
+                print('%s = %s' % (name, str(ctype)), end=' ', file=self.file)
+                print('\t# %s:%d' % (filename, lineno), file=self.file)
+        else:
+            self.known_types[name] = (ctype, filename, lineno)
+
+    def emit_type(self, t):
+        t.visit(self)
+        for s in t.get_required_type_names():
+            if s in self.known_types:
+                if s in self.linked_symbols:
+                    print('%s = %s' % (s, self.linked_symbols[s]), file=self.file)
+                else:
+                    s_ctype, s_filename, s_lineno = self.known_types[s]
+                    s_ctype.visit(self)
+
+                    self.emit_type(s_ctype)
+                    print('%s = %s' % (s, str(s_ctype)), end=' ', file=self.file)
+                    print('\t# %s:%d' % (s_filename, s_lineno), file=self.file)
+                del self.known_types[s]
+
+    def visit_struct(self, struct):
+        if struct.tag in self.structs:
+            return
+        self.structs.add(struct.tag)
+            
+        base = {True: 'Union', False: 'Structure'}[struct.is_union]
+        print('class struct_%s(%s):' % (struct.tag, base), file=self.file)
+        print('    __slots__ = [', file=self.file)
+        if not struct.opaque:
+            for m in struct.members:
+                print("        '%s'," % m[0], file=self.file)
+        print('    ]', file=self.file)
+
+        # Set fields after completing class, so incomplete structs can be
+        # referenced within struct.
+        for name, typ in struct.members:
+            self.emit_type(typ)
+
+        print('struct_%s._fields_ = [' % struct.tag, file=self.file)
+        if struct.opaque:
+            print("    ('_opaque_struct', c_int)", file=self.file)
+            self.structs.remove(struct.tag)
+        else:
+            for m in struct.members:
+                print("    ('%s', %s)," % (m[0], m[1]), file=self.file)
+        print(']', file=self.file)
+        print(file=self.file)
+
+    def visit_enum(self, enum):
+        if enum.tag in self.enums:
+            return
+        self.enums.add(enum.tag)
+
+        print('enum_%s = c_int' % enum.tag, file=self.file)
+        for name, value in enum.enumerators:
+            self.all_names.append(name)
+            print('%s = %d' % (name, value), file=self.file)
+
+    def handle_ctypes_function(self, name, restype, argtypes, filename, lineno):
+        if self.does_emit(name, filename):
+            # Also emit any types this func requires that haven't yet been
+            # written.
+            self.emit_type(restype)
+            for a in argtypes:
+                self.emit_type(a)
+
+            self.all_names.append(name)
+            print('# %s:%d' % (filename, lineno), file=self.file)
+            print('%s = _lib.%s' % (name, name), file=self.file)
+            print('%s.restype = %s' % (name, str(restype)), file=self.file)
+            print('%s.argtypes = [%s]' % \
+                (name, ', '.join([str(a) for a in argtypes])), file=self.file) 
+            print(file=self.file)
+
+    def handle_ctypes_variable(self, name, ctype, filename, lineno):
+        # This doesn't work.
+        #self.all_names.append(name)
+        #print >> self.file, '%s = %s.indll(_lib, %r)' % \
+        #    (name, str(ctype), name)
+        pass
+
+def main(*argv):
+    import optparse
+    import sys
+    import os.path
+
+    usage = 'usage: %prog [options] <header.h>'
+    op = optparse.OptionParser(usage=usage)
+    op.add_option('-o', '--output', dest='output',
+                  help='write wrapper to FILE', metavar='FILE')
+    op.add_option('-l', '--library', dest='library',
+                  help='link to LIBRARY', metavar='LIBRARY')
+    op.add_option('-D', '--define', dest='defines', default=[],
+                  help='define token NAME=VALUE', action='append')
+    op.add_option('-i', '--include-file', action='append', dest='include_files',
+                  help='assume FILE is iincluded', metavar='FILE',
+                  default=[])
+    op.add_option('-I', '--include-dir', action='append', dest='include_dirs',
+                  help='add DIR to include search path', metavar='DIR',
+                  default=[])
+    op.add_option('-m', '--link-module', action='append', dest='link_modules',
+                  help='use symbols from MODULE', metavar='MODULE',
+                  default=[])
+    op.add_option('-a', '--all-headers', action='store_true',
+                  dest='all_headers',
+                  help='include symbols from all headers', default=False)
+    
+    (options, args) = op.parse_args(list(argv[1:]))
+    if len(args) < 1:
+        print('No header files specified.', file=sys.stderr)
+        sys.exit(1)
+    headers = args
+
+    if options.library is None:
+        options.library = os.path.splitext(headers[0])[0]
+    if options.output is None:
+        options.output = '%s.py' % options.library
+
+    wrapper = CtypesWrapper()
+    wrapper.begin_output(open(options.output, 'w'), 
+                         library=options.library, 
+                         emit_filenames=headers,
+                         link_modules=options.link_modules,
+                         all_headers=options.all_headers)
+    wrapper.preprocessor_parser.include_path += options.include_dirs
+    for define in options.defines:
+        name, value = define.split('=')
+        wrapper.preprocessor_parser.define(name, value)
+    for file in options.include_files:
+        wrapper.wrap(file)
+    for header in headers:
+        wrapper.wrap(header)
+    wrapper.end_output()
+
+    print('Wrapped to %s' % options.output)
+
+if __name__ == '__main__':
+    main(*sys.argv)
diff --git a/tools/wraptypes/yacc.py b/tools/wraptypes/yacc.py
new file mode 100644
index 0000000..7952048
--- /dev/null
+++ b/tools/wraptypes/yacc.py
@@ -0,0 +1,2243 @@
+from __future__ import print_function
+#-----------------------------------------------------------------------------
+# ply: yacc.py
+#
+# Author(s): David M. Beazley (dave@dabeaz.com)
+# Modifications for pyglet by Alex Holkner (alex.holkner@gmail.com) (<ah>)
+#
+# Copyright (C) 2001-2006, David M. Beazley
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+# See the file COPYING for a complete copy of the LGPL.
+#
+#
+# This implements an LR parser that is constructed from grammar rules defined
+# as Python functions. The grammer is specified by supplying the BNF inside
+# Python documentation strings.  The inspiration for this technique was borrowed
+# from John Aycock's Spark parsing system.  PLY might be viewed as cross between
+# Spark and the GNU bison utility.
+#
+# The current implementation is only somewhat object-oriented. The
+# LR parser itself is defined in terms of an object (which allows multiple
+# parsers to co-exist).  However, most of the variables used during table
+# construction are defined in terms of global variables.  Users shouldn't
+# notice unless they are trying to define multiple parsers at the same
+# time using threads (in which case they should have their head examined).
+#
+# This implementation supports both SLR and LALR(1) parsing.  LALR(1)
+# support was originally implemented by Elias Ioup (ezioup@alumni.uchicago.edu),
+# using the algorithm found in Aho, Sethi, and Ullman "Compilers: Principles,
+# Techniques, and Tools" (The Dragon Book).  LALR(1) has since been replaced
+# by the more efficient DeRemer and Pennello algorithm.
+#
+# :::::::: WARNING :::::::
+#
+# Construction of LR parsing tables is fairly complicated and expensive.
+# To make this module run fast, a *LOT* of work has been put into
+# optimization---often at the expensive of readability and what might
+# consider to be good Python "coding style."   Modify the code at your
+# own risk!
+# ----------------------------------------------------------------------------
+
+__version__ = "2.2"
+
+#-----------------------------------------------------------------------------
+#                     === User configurable parameters ===
+#
+# Change these to modify the default behavior of yacc (if you wish)
+#-----------------------------------------------------------------------------
+
+yaccdebug   = 1                # Debugging mode.  If set, yacc generates a
+                               # a 'parser.out' file in the current directory
+
+debug_file  = 'parser.out'     # Default name of the debugging file
+tab_module  = 'parsetab'       # Default name of the table module
+default_lr  = 'LALR'           # Default LR table generation method
+
+error_count = 3                # Number of symbols that must be shifted to leave recovery mode
+
+import re, types, sys, cStringIO, md5, os.path
+
+# Exception raised for yacc-related errors
+class YaccError(Exception):   pass
+
+#-----------------------------------------------------------------------------
+#                        ===  LR Parsing Engine ===
+#
+# The following classes are used for the LR parser itself.  These are not
+# used during table construction and are independent of the actual LR
+# table generation algorithm
+#-----------------------------------------------------------------------------
+
+# This class is used to hold non-terminal grammar symbols during parsing.
+# It normally has the following attributes set:
+#        .type       = Grammar symbol type
+#        .value      = Symbol value
+#        .lineno     = Starting line number
+#        .endlineno  = Ending line number (optional, set automatically)
+#        .lexpos     = Starting lex position
+#        .endlexpos  = Ending lex position (optional, set automatically)
+
+class YaccSymbol:
+    filename = ''  # <ah>
+    def __str__(self):    return self.type
+    def __repr__(self):   return str(self)
+
+# This class is a wrapper around the objects actually passed to each
+# grammar rule.   Index lookup and assignment actually assign the
+# .value attribute of the underlying YaccSymbol object.
+# The lineno() method returns the line number of a given
+# item (or 0 if not defined).   The linespan() method returns
+# a tuple of (startline,endline) representing the range of lines
+# for a symbol.  The lexspan() method returns a tuple (lexpos,endlexpos)
+# representing the range of positional information for a symbol.
+
+class YaccProduction:
+    def __init__(self,s,stack=None):
+        self.slice = s
+        self.pbstack = []
+        self.stack = stack
+
+    def __getitem__(self,n):
+        if type(n) == types.IntType:
+             if n >= 0: return self.slice[n].value
+             else: return self.stack[n].value
+        else:
+             return [s.value for s in self.slice[n.start:n.stop:n.step]]
+
+    def __setitem__(self,n,v):
+        self.slice[n].value = v
+
+    def __len__(self):
+        return len(self.slice)
+
+    def lineno(self,n):
+        return getattr(self.slice[n],"lineno",0)
+
+    def linespan(self,n):
+        startline = getattr(self.slice[n],"lineno",0)
+        endline = getattr(self.slice[n],"endlineno",startline)
+        return startline,endline
+
+    def lexpos(self,n):
+        return getattr(self.slice[n],"lexpos",0)
+
+    def lexspan(self,n):
+        startpos = getattr(self.slice[n],"lexpos",0)
+        endpos = getattr(self.slice[n],"endlexpos",startpos)
+        return startpos,endpos
+
+    def pushback(self,n):
+        if n <= 0:
+            raise ValueError("Expected a positive value")
+        if n > (len(self.slice)-1):
+            raise ValueError("Can't push %d tokens. Only %d are available." % (n,len(self.slice)-1))
+        for i in range(0,n):
+            self.pbstack.append(self.slice[-i-1])
+
+# The LR Parsing engine.   This is defined as a class so that multiple parsers
+# can exist in the same process.  A user never instantiates this directly.
+# Instead, the global yacc() function should be used to create a suitable Parser
+# object.
+
+class Parser:
+    # <ah> Remove magic (use ParserPrototype)
+    def __init__(self):
+        # Reset internal state
+        self.productions = None          # List of productions
+        self.errorfunc   = None          # Error handling function
+        self.action      = { }           # LR Action table
+        self.goto        = { }           # LR goto table
+        self.require     = { }           # Attribute require table
+        self.method      = "Unknown LR"  # Table construction method used
+
+        # <ah> 25 Jan 2007
+        self.statestackstack = []
+        self.symstackstack = []
+
+    def errok(self):
+        self.errorcount = 0
+
+    def restart(self):
+        del self.statestack[:]
+        del self.symstack[:]
+        sym = YaccSymbol()
+        sym.type = '$end'
+        self.symstack.append(sym)
+        self.statestack.append(0)
+
+    def push_state(self):
+        '''Save parser state and restart it.'''
+        # <ah> 25 Jan 2007
+        self.statestackstack.append(self.statestack[:])
+        self.symstackstack.append(self.symstack[:])
+        self.restart()
+
+    def pop_state(self):
+        '''Restore saved parser state.'''
+        # <ah> 25 Jan 2007
+        self.statestack[:] = self.statestackstack.pop()
+        self.symstack[:] = self.symstackstack.pop()
+
+    def parse(self,input=None,lexer=None,debug=0):
+        lookahead = None                 # Current lookahead symbol
+        lookaheadstack = [ ]             # Stack of lookahead symbols
+        actions = self.action            # Local reference to action table
+        goto    = self.goto              # Local reference to goto table
+        prod    = self.productions       # Local reference to production list
+        pslice  = YaccProduction(None)   # Production object passed to grammar rules
+        pslice.parser = self             # Parser object
+        self.errorcount = 0              # Used during error recovery
+
+        # If no lexer was given, we will try to use the lex module
+        if not lexer:
+            import lex
+            lexer = lex.lexer
+
+        pslice.lexer = lexer
+
+        # If input was supplied, pass to lexer
+        if input:
+            lexer.input(input)
+
+        # Tokenize function
+        get_token = lexer.token
+
+        statestack = [ ]                # Stack of parsing states
+        self.statestack = statestack
+        symstack   = [ ]                # Stack of grammar symbols
+        self.symstack = symstack
+
+        pslice.stack = symstack         # Put in the production
+        errtoken   = None               # Err token
+
+        # The start state is assumed to be (0,$end)
+        statestack.append(0)
+        sym = YaccSymbol()
+        sym.type = '$end'
+        symstack.append(sym)
+
+        while 1:
+            # Get the next symbol on the input.  If a lookahead symbol
+            # is already set, we just use that. Otherwise, we'll pull
+            # the next token off of the lookaheadstack or from the lexer
+            if debug > 1:
+                print('state', statestack[-1])
+            if not lookahead:
+                if not lookaheadstack:
+                    lookahead = get_token()     # Get the next token
+                else:
+                    lookahead = lookaheadstack.pop()
+                if not lookahead:
+                    lookahead = YaccSymbol()
+                    lookahead.type = '$end'
+            if debug:
+                errorlead = ("%s . %s" % (" ".join([xx.type for xx in symstack][1:]), str(lookahead))).lstrip()
+
+            # Check the action table
+            s = statestack[-1]
+            ltype = lookahead.type
+            t = actions.get((s,ltype),None)
+
+            if debug > 1:
+                print('action', t)
+            if t is not None:
+                if t > 0:
+                    # shift a symbol on the stack
+                    if ltype == '$end':
+                        # Error, end of input
+                        sys.stderr.write("yacc: Parse error. EOF\n")
+                        return
+                    statestack.append(t)
+                    if debug > 1:
+                        sys.stderr.write("%-60s shift state %s\n" % (errorlead, t))
+                    symstack.append(lookahead)
+                    lookahead = None
+
+                    # Decrease error count on successful shift
+                    if self.errorcount > 0:
+                        self.errorcount -= 1
+
+                    continue
+
+                if t < 0:
+                    # reduce a symbol on the stack, emit a production
+                    p = prod[-t]
+                    pname = p.name
+                    plen  = p.len
+
+                    # Get production function
+                    sym = YaccSymbol()
+                    sym.type = pname       # Production name
+                    sym.value = None
+                    if debug > 1:
+                        sys.stderr.write("%-60s reduce %d\n" % (errorlead, -t))
+
+                    if plen:
+                        targ = symstack[-plen-1:]
+                        targ[0] = sym
+                        try:
+                            sym.lineno = targ[1].lineno
+                            sym.filename = targ[1].filename
+                            sym.endlineno = getattr(targ[-1],"endlineno",targ[-1].lineno)
+                            sym.lexpos = targ[1].lexpos
+                            sym.endlexpos = getattr(targ[-1],"endlexpos",targ[-1].lexpos)
+                        except AttributeError:
+                            sym.lineno = 0
+                        del symstack[-plen:]
+                        del statestack[-plen:]
+                    else:
+                        sym.lineno = 0
+                        targ = [ sym ]
+                    pslice.slice = targ
+                    pslice.pbstack = []
+                    # Call the grammar rule with our special slice object
+                    p.func(pslice)
+
+                    # If there was a pushback, put that on the stack
+                    if pslice.pbstack:
+                        lookaheadstack.append(lookahead)
+                        for _t in pslice.pbstack:
+                            lookaheadstack.append(_t)
+                        lookahead = None
+
+                    symstack.append(sym)
+                    statestack.append(goto[statestack[-1],pname])
+                    continue
+
+                if t == 0:
+                    n = symstack[-1]
+                    return getattr(n,"value",None)
+                    sys.stderr.write(errorlead, "\n")
+
+            if t == None:
+                if debug:
+                    sys.stderr.write(errorlead + "\n")
+                # We have some kind of parsing error here.  To handle
+                # this, we are going to push the current token onto
+                # the tokenstack and replace it with an 'error' token.
+                # If there are any synchronization rules, they may
+                # catch it.
+                #
+                # In addition to pushing the error token, we call call
+                # the user defined p_error() function if this is the
+                # first syntax error.  This function is only called if
+                # errorcount == 0.
+                if not self.errorcount:
+                    self.errorcount = error_count
+                    errtoken = lookahead
+                    if errtoken.type == '$end':
+                        errtoken = None               # End of file!
+                    if self.errorfunc:
+                        global errok,token,restart
+                        errok = self.errok        # Set some special functions available in error recovery
+                        token = get_token
+                        restart = self.restart
+                        tok = self.errorfunc(errtoken)
+                        del errok, token, restart   # Delete special functions
+
+                        if not self.errorcount:
+                            # User must have done some kind of panic
+                            # mode recovery on their own.  The
+                            # returned token is the next lookahead
+                            lookahead = tok
+                            errtoken = None
+                            continue
+                    else:
+                        if errtoken:
+                            if hasattr(errtoken,"lineno"): lineno = lookahead.lineno
+                            else: lineno = 0
+                            if lineno:
+                                sys.stderr.write("yacc: Syntax error at line %d, token=%s\n" % (lineno, errtoken.type))
+                            else:
+                                sys.stderr.write("yacc: Syntax error, token=%s" % errtoken.type)
+                        else:
+                            sys.stderr.write("yacc: Parse error in input. EOF\n")
+                            return
+
+                else:
+                    self.errorcount = error_count
+
+                # case 1:  the statestack only has 1 entry on it.  If we're in this state, the
+                # entire parse has been rolled back and we're completely hosed.   The token is
+                # discarded and we just keep going.
+
+                if len(statestack) <= 1 and lookahead.type != '$end':
+                    lookahead = None
+                    errtoken = None
+                    # Nuke the pushback stack
+                    del lookaheadstack[:]
+                    continue
+
+                # case 2: the statestack has a couple of entries on it, but we're
+                # at the end of the file. nuke the top entry and generate an error token
+
+                # Start nuking entries on the stack
+                if lookahead.type == '$end':
+                    # Whoa. We're really hosed here. Bail out
+                    return
+
+                if lookahead.type != 'error':
+                    sym = symstack[-1]
+                    if sym.type == 'error':
+                        # Hmmm. Error is on top of stack, we'll just nuke input
+                        # symbol and continue
+                        lookahead = None
+                        continue
+                    t = YaccSymbol()
+                    t.type = 'error'
+                    if hasattr(lookahead,"lineno"):
+                        t.lineno = lookahead.lineno
+                    t.value = lookahead
+                    lookaheadstack.append(lookahead)
+                    lookahead = t
+                else:
+                    symstack.pop()
+                    statestack.pop()
+
+                continue
+
+            # Call an error function here
+            raise RuntimeError("yacc: internal parser error!!!\n")
+
+# -----------------------------------------------------------------------------
+#                          === Parser Construction ===
+#
+# The following functions and variables are used to implement the yacc() function
+# itself.   This is pretty hairy stuff involving lots of error checking,
+# construction of LR items, kernels, and so forth.   Although a lot of
+# this work is done using global variables, the resulting Parser object
+# is completely self contained--meaning that it is safe to repeatedly
+# call yacc() with different grammars in the same application.
+# -----------------------------------------------------------------------------
+
+# -----------------------------------------------------------------------------
+# validate_file()
+#
+# This function checks to see if there are duplicated p_rulename() functions
+# in the parser module file.  Without this function, it is really easy for
+# users to make mistakes by cutting and pasting code fragments (and it's a real
+# bugger to try and figure out why the resulting parser doesn't work).  Therefore,
+# we just do a little regular expression pattern matching of def statements
+# to try and detect duplicates.
+# -----------------------------------------------------------------------------
+
+def validate_file(filename):
+    base,ext = os.path.splitext(filename)
+    if ext != '.py': return 1          # No idea. Assume it's okay.
+
+    try:
+        f = open(filename)
+        lines = f.readlines()
+        f.close()
+    except IOError:
+        return 1                       # Oh well
+
+    # Match def p_funcname(
+    fre = re.compile(r'\s*def\s+(p_[a-zA-Z_0-9]*)\(')
+    counthash = { }
+    linen = 1
+    noerror = 1
+    for l in lines:
+        m = fre.match(l)
+        if m:
+            name = m.group(1)
+            prev = counthash.get(name)
+            if not prev:
+                counthash[name] = linen
+            else:
+                sys.stderr.write("%s:%d: Function %s redefined. Previously defined on line %d\n" % (filename,linen,name,prev))
+                noerror = 0
+        linen += 1
+    return noerror
+
+# This function looks for functions that might be grammar rules, but which don't have the proper p_suffix.
+def validate_dict(d):
+    for n,v in d.items():
+        if n[0:2] == 'p_' and type(v) in (types.FunctionType, types.MethodType): continue
+        if n[0:2] == 't_': continue
+
+        if n[0:2] == 'p_':
+            sys.stderr.write("yacc: Warning. '%s' not defined as a function\n" % n)
+        if 1 and isinstance(v,types.FunctionType) and v.func_code.co_argcount == 1:
+            try:
+                doc = v.__doc__.split(" ")
+                if doc[1] == ':':
+                    sys.stderr.write("%s:%d: Warning. Possible grammar rule '%s' defined without p_ prefix.\n" % (v.func_code.co_filename, v.func_code.co_firstlineno,n))
+            except Exception:
+                pass
+
+# -----------------------------------------------------------------------------
+#                           === GRAMMAR FUNCTIONS ===
+#
+# The following global variables and functions are used to store, manipulate,
+# and verify the grammar rules specified by the user.
+# -----------------------------------------------------------------------------
+
+# Initialize all of the global variables used during grammar construction
+def initialize_vars():
+    global Productions, Prodnames, Prodmap, Terminals
+    global Nonterminals, First, Follow, Precedence, LRitems
+    global Errorfunc, Signature, Requires
+
+    Productions  = [None]  # A list of all of the productions.  The first
+                           # entry is always reserved for the purpose of
+                           # building an augmented grammar
+
+    Prodnames    = { }     # A dictionary mapping the names of nonterminals to a list of all
+                           # productions of that nonterminal.
+
+    Prodmap      = { }     # A dictionary that is only used to detect duplicate
+                           # productions.
+
+    Terminals    = { }     # A dictionary mapping the names of terminal symbols to a
+                           # list of the rules where they are used.
+
+    Nonterminals = { }     # A dictionary mapping names of nonterminals to a list
+                           # of rule numbers where they are used.
+
+    First        = { }     # A dictionary of precomputed FIRST(x) symbols
+
+    Follow       = { }     # A dictionary of precomputed FOLLOW(x) symbols
+
+    Precedence   = { }     # Precedence rules for each terminal. Contains tuples of the
+                           # form ('right',level) or ('nonassoc', level) or ('left',level)
+
+    LRitems      = [ ]     # A list of all LR items for the grammar.  These are the
+                           # productions with the "dot" like E -> E . PLUS E
+
+    Errorfunc    = None    # User defined error handler
+
+    Signature    = md5.new()   # Digital signature of the grammar rules, precedence
+                               # and other information.  Used to determined when a
+                               # parsing table needs to be regenerated.
+
+    Requires     = { }     # Requires list
+
+    # File objects used when creating the parser.out debugging file
+    global _vf, _vfc
+    _vf           = cStringIO.StringIO()
+    _vfc          = cStringIO.StringIO()
+
+# -----------------------------------------------------------------------------
+# class Production:
+#
+# This class stores the raw information about a single production or grammar rule.
+# It has a few required attributes:
+#
+#       name     - Name of the production (nonterminal)
+#       prod     - A list of symbols making up its production
+#       number   - Production number.
+#
+# In addition, a few additional attributes are used to help with debugging or
+# optimization of table generation.
+#
+#       file     - File where production action is defined.
+#       lineno   - Line number where action is defined
+#       func     - Action function
+#       prec     - Precedence level
+#       lr_next  - Next LR item. Example, if we are ' E -> E . PLUS E'
+#                  then lr_next refers to 'E -> E PLUS . E'
+#       lr_index - LR item index (location of the ".") in the prod list.
+#       lookaheads - LALR lookahead symbols for this item
+#       len      - Length of the production (number of symbols on right hand side)
+# -----------------------------------------------------------------------------
+
+class Production:
+    def __init__(self,**kw):
+        for k,v in kw.items():
+            setattr(self,k,v)
+        self.lr_index = -1
+        self.lr0_added = 0    # Flag indicating whether or not added to LR0 closure
+        self.lr1_added = 0    # Flag indicating whether or not added to LR1
+        self.usyms = [ ]
+        self.lookaheads = { }
+        self.lk_added = { }
+        self.setnumbers = [ ]
+
+    def __str__(self):
+        if self.prod:
+            s = "%s -> %s" % (self.name," ".join(self.prod))
+        else:
+            s = "%s -> <empty>" % self.name
+        return s
+
+    def __repr__(self):
+        return str(self)
+
+    # Compute lr_items from the production
+    def lr_item(self,n):
+        if n > len(self.prod): return None
+        p = Production()
+        p.name = self.name
+        p.prod = list(self.prod)
+        p.number = self.number
+        p.lr_index = n
+        p.lookaheads = { }
+        p.setnumbers = self.setnumbers
+        p.prod.insert(n,".")
+        p.prod = tuple(p.prod)
+        p.len = len(p.prod)
+        p.usyms = self.usyms
+
+        # Precompute list of productions immediately following
+        try:
+            p.lrafter = Prodnames[p.prod[n+1]]
+        except (IndexError,KeyError) as e:
+            p.lrafter = []
+        try:
+            p.lrbefore = p.prod[n-1]
+        except IndexError:
+            p.lrbefore = None
+
+        return p
+
+class MiniProduction:
+    pass
+
+# regex matching identifiers
+_is_identifier = re.compile(r'^[a-zA-Z0-9_-]+$')
+
+# -----------------------------------------------------------------------------
+# add_production()
+#
+# Given an action function, this function assembles a production rule.
+# The production rule is assumed to be found in the function's docstring.
+# This rule has the general syntax:
+#
+#              name1 ::= production1
+#                     |  production2
+#                     |  production3
+#                    ...
+#                     |  productionn
+#              name2 ::= production1
+#                     |  production2
+#                    ...
+# -----------------------------------------------------------------------------
+
+def add_production(f,file,line,prodname,syms):
+
+    if Terminals.has_key(prodname):
+        sys.stderr.write("%s:%d: Illegal rule name '%s'. Already defined as a token.\n" % (file,line,prodname))
+        return -1
+    if prodname == 'error':
+        sys.stderr.write("%s:%d: Illegal rule name '%s'. error is a reserved word.\n" % (file,line,prodname))
+        return -1
+
+    if not _is_identifier.match(prodname):
+        sys.stderr.write("%s:%d: Illegal rule name '%s'\n" % (file,line,prodname))
+        return -1
+
+    for x in range(len(syms)):
+        s = syms[x]
+        if s[0] in "'\"":
+             try:
+                 c = eval(s)
+                 if (len(c) > 1):
+                      sys.stderr.write("%s:%d: Literal token %s in rule '%s' may only be a single character\n" % (file,line,s, prodname))
+                      return -1
+                 if not Terminals.has_key(c):
+                      Terminals[c] = []
+                 syms[x] = c
+                 continue
+             except SyntaxError:
+                 pass
+        if not _is_identifier.match(s) and s != '%prec':
+            sys.stderr.write("%s:%d: Illegal name '%s' in rule '%s'\n" % (file,line,s, prodname))
+            return -1
+
+    # See if the rule is already in the rulemap
+    map = "%s -> %s" % (prodname,syms)
+    if Prodmap.has_key(map):
+        m = Prodmap[map]
+        sys.stderr.write("%s:%d: Duplicate rule %s.\n" % (file,line, m))
+        sys.stderr.write("%s:%d: Previous definition at %s:%d\n" % (file,line, m.file, m.line))
+        return -1
+
+    p = Production()
+    p.name = prodname
+    p.prod = syms
+    p.file = file
+    p.line = line
+    p.func = f
+    p.number = len(Productions)
+
+
+    Productions.append(p)
+    Prodmap[map] = p
+    if not Nonterminals.has_key(prodname):
+        Nonterminals[prodname] = [ ]
+
+    # Add all terminals to Terminals
+    i = 0
+    while i < len(p.prod):
+        t = p.prod[i]
+        if t == '%prec':
+            try:
+                precname = p.prod[i+1]
+            except IndexError:
+                sys.stderr.write("%s:%d: Syntax error. Nothing follows %%prec.\n" % (p.file,p.line))
+                return -1
+
+            prec = Precedence.get(precname,None)
+            if not prec:
+                sys.stderr.write("%s:%d: Nothing known about the precedence of '%s'\n" % (p.file,p.line,precname))
+                return -1
+            else:
+                p.prec = prec
+            del p.prod[i]
+            del p.prod[i]
+            continue
+
+        if Terminals.has_key(t):
+            Terminals[t].append(p.number)
+            # Is a terminal.  We'll assign a precedence to p based on this
+            if not hasattr(p,"prec"):
+                p.prec = Precedence.get(t,('right',0))
+        else:
+            if not Nonterminals.has_key(t):
+                Nonterminals[t] = [ ]
+            Nonterminals[t].append(p.number)
+        i += 1
+
+    if not hasattr(p,"prec"):
+        p.prec = ('right',0)
+
+    # Set final length of productions
+    p.len  = len(p.prod)
+    p.prod = tuple(p.prod)
+
+    # Calculate unique syms in the production
+    p.usyms = [ ]
+    for s in p.prod:
+        if s not in p.usyms:
+            p.usyms.append(s)
+
+    # Add to the global productions list
+    try:
+        Prodnames[p.name].append(p)
+    except KeyError:
+        Prodnames[p.name] = [ p ]
+    return 0
+
+# Given a raw rule function, this function rips out its doc string
+# and adds rules to the grammar
+
+def add_function(f):
+    line = f.func_code.co_firstlineno
+    file = f.func_code.co_filename
+    error = 0
+
+    if isinstance(f,types.MethodType):
+        reqdargs = 2
+    else:
+        reqdargs = 1
+
+    if f.func_code.co_argcount > reqdargs:
+        sys.stderr.write("%s:%d: Rule '%s' has too many arguments.\n" % (file,line,f.__name__))
+        return -1
+
+    if f.func_code.co_argcount < reqdargs:
+        sys.stderr.write("%s:%d: Rule '%s' requires an argument.\n" % (file,line,f.__name__))
+        return -1
+
+    if f.__doc__:
+        # Split the doc string into lines
+        pstrings = f.__doc__.splitlines()
+        lastp = None
+        dline = line
+        for ps in pstrings:
+            dline += 1
+            p = ps.split()
+            if not p: continue
+            try:
+                if p[0] == '|':
+                    # This is a continuation of a previous rule
+                    if not lastp:
+                        sys.stderr.write("%s:%d: Misplaced '|'.\n" % (file,dline))
+                        return -1
+                    prodname = lastp
+                    if len(p) > 1:
+                        syms = p[1:]
+                    else:
+                        syms = [ ]
+                else:
+                    prodname = p[0]
+                    lastp = prodname
+                    assign = p[1]
+                    if len(p) > 2:
+                        syms = p[2:]
+                    else:
+                        syms = [ ]
+                    if assign != ':' and assign != '::=':
+                        sys.stderr.write("%s:%d: Syntax error. Expected ':'\n" % (file,dline))
+                        return -1
+
+
+                e = add_production(f,file,dline,prodname,syms)
+                error += e
+
+
+            except Exception:
+                sys.stderr.write("%s:%d: Syntax error in rule '%s'\n" % (file,dline,ps))
+                error -= 1
+    else:
+        sys.stderr.write("%s:%d: No documentation string specified in function '%s'\n" % (file,line,f.__name__))
+    return error
+
+
+# Cycle checking code (Michael Dyck)
+
+def compute_reachable():
+    '''
+    Find each symbol that can be reached from the start symbol.
+    Print a warning for any nonterminals that can't be reached.
+    (Unused terminals have already had their warning.)
+    '''
+    Reachable = { }
+    for s in Terminals.keys() + Nonterminals.keys():
+        Reachable[s] = 0
+
+    mark_reachable_from( Productions[0].prod[0], Reachable )
+
+    for s in Nonterminals.keys():
+        if not Reachable[s]:
+            sys.stderr.write("yacc: Symbol '%s' is unreachable.\n" % s)
+
+def mark_reachable_from(s, Reachable):
+    '''
+    Mark all symbols that are reachable from symbol s.
+    '''
+    if Reachable[s]:
+        # We've already reached symbol s.
+        return
+    Reachable[s] = 1
+    for p in Prodnames.get(s,[]):
+        for r in p.prod:
+            mark_reachable_from(r, Reachable)
+
+# -----------------------------------------------------------------------------
+# compute_terminates()
+#
+# This function looks at the various parsing rules and tries to detect
+# infinite recursion cycles (grammar rules where there is no possible way
+# to derive a string of only terminals).
+# -----------------------------------------------------------------------------
+def compute_terminates():
+    '''
+    Raise an error for any symbols that don't terminate.
+    '''
+    Terminates = {}
+
+    # Terminals:
+    for t in Terminals.keys():
+        Terminates[t] = 1
+
+    Terminates['$end'] = 1
+
+    # Nonterminals:
+
+    # Initialize to false:
+    for n in Nonterminals.keys():
+        Terminates[n] = 0
+
+    # Then propagate termination until no change:
+    while 1:
+        some_change = 0
+        for (n,pl) in Prodnames.items():
+            # Nonterminal n terminates iff any of its productions terminates.
+            for p in pl:
+                # Production p terminates iff all of its rhs symbols terminate.
+                for s in p.prod:
+                    if not Terminates[s]:
+                        # The symbol s does not terminate,
+                        # so production p does not terminate.
+                        p_terminates = 0
+                        break
+                else:
+                    # didn't break from the loop,
+                    # so every symbol s terminates
+                    # so production p terminates.
+                    p_terminates = 1
+
+                if p_terminates:
+                    # symbol n terminates!
+                    if not Terminates[n]:
+                        Terminates[n] = 1
+                        some_change = 1
+                    # Don't need to consider any more productions for this n.
+                    break
+
+        if not some_change:
+            break
+
+    some_error = 0
+    for (s,terminates) in Terminates.items():
+        if not terminates:
+            if not Prodnames.has_key(s) and not Terminals.has_key(s) and s != 'error':
+                # s is used-but-not-defined, and we've already warned of that,
+                # so it would be overkill to say that it's also non-terminating.
+                pass
+            else:
+                sys.stderr.write("yacc: Infinite recursion detected for symbol '%s'.\n" % s)
+                some_error = 1
+
+    return some_error
+
+# -----------------------------------------------------------------------------
+# verify_productions()
+#
+# This function examines all of the supplied rules to see if they seem valid.
+# -----------------------------------------------------------------------------
+def verify_productions(cycle_check=1):
+    error = 0
+    for p in Productions:
+        if not p: continue
+
+        for s in p.prod:
+            if not Prodnames.has_key(s) and not Terminals.has_key(s) and s != 'error':
+                sys.stderr.write("%s:%d: Symbol '%s' used, but not defined as a token or a rule.\n" % (p.file,p.line,s))
+                error = 1
+                continue
+
+    unused_tok = 0
+    # Now verify all of the tokens
+    if yaccdebug:
+        _vf.write("Unused terminals:\n\n")
+    for s,v in Terminals.items():
+        if s != 'error' and not v:
+            sys.stderr.write("yacc: Warning. Token '%s' defined, but not used.\n" % s)
+            if yaccdebug: _vf.write("   %s\n"% s)
+            unused_tok += 1
+
+    # Print out all of the productions
+    if yaccdebug:
+        _vf.write("\nGrammar\n\n")
+        for i in range(1,len(Productions)):
+            _vf.write("Rule %-5d %s\n" % (i, Productions[i]))
+
+    unused_prod = 0
+    # Verify the use of all productions
+    for s,v in Nonterminals.items():
+        if not v:
+            p = Prodnames[s][0]
+            sys.stderr.write("%s:%d: Warning. Rule '%s' defined, but not used.\n" % (p.file,p.line, s))
+            unused_prod += 1
+
+
+    if unused_tok == 1:
+        sys.stderr.write("yacc: Warning. There is 1 unused token.\n")
+    if unused_tok > 1:
+        sys.stderr.write("yacc: Warning. There are %d unused tokens.\n" % unused_tok)
+
+    if unused_prod == 1:
+        sys.stderr.write("yacc: Warning. There is 1 unused rule.\n")
+    if unused_prod > 1:
+        sys.stderr.write("yacc: Warning. There are %d unused rules.\n" % unused_prod)
+
+    if yaccdebug:
+        _vf.write("\nTerminals, with rules where they appear\n\n")
+        ks = Terminals.keys()
+        ks.sort()
+        for k in ks:
+            _vf.write("%-20s : %s\n" % (k, " ".join([str(s) for s in Terminals[k]])))
+        _vf.write("\nNonterminals, with rules where they appear\n\n")
+        ks = Nonterminals.keys()
+        ks.sort()
+        for k in ks:
+            _vf.write("%-20s : %s\n" % (k, " ".join([str(s) for s in Nonterminals[k]])))
+
+    if (cycle_check):
+        compute_reachable()
+        error += compute_terminates()
+#        error += check_cycles()
+    return error
+
+# -----------------------------------------------------------------------------
+# build_lritems()
+#
+# This function walks the list of productions and builds a complete set of the
+# LR items.  The LR items are stored in two ways:  First, they are uniquely
+# numbered and placed in the list _lritems.  Second, a linked list of LR items
+# is built for each production.  For example:
+#
+#   E -> E PLUS E
+#
+# Creates the list
+#
+#  [E -> . E PLUS E, E -> E . PLUS E, E -> E PLUS . E, E -> E PLUS E . ]
+# -----------------------------------------------------------------------------
+
+def build_lritems():
+    for p in Productions:
+        lastlri = p
+        lri = p.lr_item(0)
+        i = 0
+        while 1:
+            lri = p.lr_item(i)
+            lastlri.lr_next = lri
+            if not lri: break
+            lri.lr_num = len(LRitems)
+            LRitems.append(lri)
+            lastlri = lri
+            i += 1
+
+    # In order for the rest of the parser generator to work, we need to
+    # guarantee that no more lritems are generated.  Therefore, we nuke
+    # the p.lr_item method.  (Only used in debugging)
+    # Production.lr_item = None
+
+# -----------------------------------------------------------------------------
+# add_precedence()
+#
+# Given a list of precedence rules, add to the precedence table.
+# -----------------------------------------------------------------------------
+
+def add_precedence(plist):
+    plevel = 0
+    error = 0
+    for p in plist:
+        plevel += 1
+        try:
+            prec = p[0]
+            terms = p[1:]
+            if prec != 'left' and prec != 'right' and prec != 'nonassoc':
+                sys.stderr.write("yacc: Invalid precedence '%s'\n" % prec)
+                return -1
+            for t in terms:
+                if Precedence.has_key(t):
+                    sys.stderr.write("yacc: Precedence already specified for terminal '%s'\n" % t)
+                    error += 1
+                    continue
+                Precedence[t] = (prec,plevel)
+        except:
+            sys.stderr.write("yacc: Invalid precedence table.\n")
+            error += 1
+
+    return error
+
+# -----------------------------------------------------------------------------
+# augment_grammar()
+#
+# Compute the augmented grammar.  This is just a rule S' -> start where start
+# is the starting symbol.
+# -----------------------------------------------------------------------------
+
+def augment_grammar(start=None):
+    if not start:
+        start = Productions[1].name
+    Productions[0] = Production(name="S'",prod=[start],number=0,len=1,prec=('right',0),func=None)
+    Productions[0].usyms = [ start ]
+    Nonterminals[start].append(0)
+
+
+# -------------------------------------------------------------------------
+# first()
+#
+# Compute the value of FIRST1(beta) where beta is a tuple of symbols.
+#
+# During execution of compute_first1, the result may be incomplete.
+# Afterward (e.g., when called from compute_follow()), it will be complete.
+# -------------------------------------------------------------------------
+def first(beta):
+
+    # We are computing First(x1,x2,x3,...,xn)
+    result = [ ]
+    for x in beta:
+        x_produces_empty = 0
+
+        # Add all the non-<empty> symbols of First[x] to the result.
+        for f in First[x]:
+            if f == '<empty>':
+                x_produces_empty = 1
+            else:
+                if f not in result: result.append(f)
+
+        if x_produces_empty:
+            # We have to consider the next x in beta,
+            # i.e. stay in the loop.
+            pass
+        else:
+            # We don't have to consider any further symbols in beta.
+            break
+    else:
+        # There was no 'break' from the loop,
+        # so x_produces_empty was true for all x in beta,
+        # so beta produces empty as well.
+        result.append('<empty>')
+
+    return result
+
+
+# FOLLOW(x)
+# Given a non-terminal.  This function computes the set of all symbols
+# that might follow it.  Dragon book, p. 189.
+
+def compute_follow(start=None):
+    # Add '$end' to the follow list of the start symbol
+    for k in Nonterminals.keys():
+        Follow[k] = [ ]
+
+    if not start:
+        start = Productions[1].name
+
+    Follow[start] = [ '$end' ]
+
+    while 1:
+        didadd = 0
+        for p in Productions[1:]:
+            # Here is the production set
+            for i in range(len(p.prod)):
+                B = p.prod[i]
+                if Nonterminals.has_key(B):
+                    # Okay. We got a non-terminal in a production
+                    fst = first(p.prod[i+1:])
+                    hasempty = 0
+                    for f in fst:
+                        if f != '<empty>' and f not in Follow[B]:
+                            Follow[B].append(f)
+                            didadd = 1
+                        if f == '<empty>':
+                            hasempty = 1
+                    if hasempty or i == (len(p.prod)-1):
+                        # Add elements of follow(a) to follow(b)
+                        for f in Follow[p.name]:
+                            if f not in Follow[B]:
+                                Follow[B].append(f)
+                                didadd = 1
+        if not didadd: break
+
+    if 0 and yaccdebug:
+        _vf.write('\nFollow:\n')
+        for k in Nonterminals.keys():
+            _vf.write("%-20s : %s\n" % (k, " ".join([str(s) for s in Follow[k]])))
+
+# -------------------------------------------------------------------------
+# compute_first1()
+#
+# Compute the value of FIRST1(X) for all symbols
+# -------------------------------------------------------------------------
+def compute_first1():
+
+    # Terminals:
+    for t in Terminals.keys():
+        First[t] = [t]
+
+    First['$end'] = ['$end']
+    First['#'] = ['#'] # what's this for?
+
+    # Nonterminals:
+
+    # Initialize to the empty set:
+    for n in Nonterminals.keys():
+        First[n] = []
+
+    # Then propagate symbols until no change:
+    while 1:
+        some_change = 0
+        for n in Nonterminals.keys():
+            for p in Prodnames[n]:
+                for f in first(p.prod):
+                    if f not in First[n]:
+                        First[n].append( f )
+                        some_change = 1
+        if not some_change:
+            break
+
+    if 0 and yaccdebug:
+        _vf.write('\nFirst:\n')
+        for k in Nonterminals.keys():
+            _vf.write("%-20s : %s\n" %
+                (k, " ".join([str(s) for s in First[k]])))
+
+# -----------------------------------------------------------------------------
+#                           === SLR Generation ===
+#
+# The following functions are used to construct SLR (Simple LR) parsing tables
+# as described on p.221-229 of the dragon book.
+# -----------------------------------------------------------------------------
+
+# Global variables for the LR parsing engine
+def lr_init_vars():
+    global _lr_action, _lr_goto, _lr_method
+    global _lr_goto_cache, _lr0_cidhash
+
+    _lr_action       = { }        # Action table
+    _lr_goto         = { }        # Goto table
+    _lr_method       = "Unknown"  # LR method used
+    _lr_goto_cache   = { }
+    _lr0_cidhash     = { }
+
+
+# Compute the LR(0) closure operation on I, where I is a set of LR(0) items.
+# prodlist is a list of productions.
+
+_add_count = 0       # Counter used to detect cycles
+
+def lr0_closure(I):
+    global _add_count
+
+    _add_count += 1
+    prodlist = Productions
+
+    # Add everything in I to J
+    J = I[:]
+    didadd = 1
+    while didadd:
+        didadd = 0
+        for j in J:
+            for x in j.lrafter:
+                if x.lr0_added == _add_count: continue
+                # Add B --> .G to J
+                J.append(x.lr_next)
+                x.lr0_added = _add_count
+                didadd = 1
+
+    return J
+
+# Compute the LR(0) goto function goto(I,X) where I is a set
+# of LR(0) items and X is a grammar symbol.   This function is written
+# in a way that guarantees uniqueness of the generated goto sets
+# (i.e. the same goto set will never be returned as two different Python
+# objects).  With uniqueness, we can later do fast set comparisons using
+# id(obj) instead of element-wise comparison.
+
+def lr0_goto(I,x):
+    # First we look for a previously cached entry
+    g = _lr_goto_cache.get((id(I),x),None)
+    if g: return g
+
+    # Now we generate the goto set in a way that guarantees uniqueness
+    # of the result
+
+    s = _lr_goto_cache.get(x,None)
+    if not s:
+        s = { }
+        _lr_goto_cache[x] = s
+
+    gs = [ ]
+    for p in I:
+        n = p.lr_next
+        if n and n.lrbefore == x:
+            s1 = s.get(id(n),None)
+            if not s1:
+                s1 = { }
+                s[id(n)] = s1
+            gs.append(n)
+            s = s1
+    g = s.get('$end',None)
+    if not g:
+        if gs:
+            g = lr0_closure(gs)
+            s['$end'] = g
+        else:
+            s['$end'] = gs
+    _lr_goto_cache[(id(I),x)] = g
+    return g
+
+_lr0_cidhash = { }
+
+# Compute the LR(0) sets of item function
+def lr0_items():
+
+    C = [ lr0_closure([Productions[0].lr_next]) ]
+    i = 0
+    for I in C:
+        _lr0_cidhash[id(I)] = i
+        i += 1
+
+    # Loop over the items in C and each grammar symbols
+    i = 0
+    while i < len(C):
+        I = C[i]
+        i += 1
+
+        # Collect all of the symbols that could possibly be in the goto(I,X) sets
+        asyms = { }
+        for ii in I:
+            for s in ii.usyms:
+                asyms[s] = None
+
+        for x in asyms.keys():
+            g = lr0_goto(I,x)
+            if not g:  continue
+            if _lr0_cidhash.has_key(id(g)): continue
+            _lr0_cidhash[id(g)] = len(C)
+            C.append(g)
+
+    return C
+
+# -----------------------------------------------------------------------------
+#                       ==== LALR(1) Parsing ====
+#
+# LALR(1) parsing is almost exactly the same as SLR except that instead of
+# relying upon Follow() sets when performing reductions, a more selective
+# lookahead set that incorporates the state of the LR(0) machine is utilized.
+# Thus, we mainly just have to focus on calculating the lookahead sets.
+#
+# The method used here is due to DeRemer and Pennelo (1982).
+#
+# DeRemer, F. L., and T. J. Pennelo: "Efficient Computation of LALR(1)
+#     Lookahead Sets", ACM Transactions on Programming Languages and Systems,
+#     Vol. 4, No. 4, Oct. 1982, pp. 615-649
+#
+# Further details can also be found in:
+#
+#  J. Tremblay and P. Sorenson, "The Theory and Practice of Compiler Writing",
+#      McGraw-Hill Book Company, (1985).
+#
+# Note:  This implementation is a complete replacement of the LALR(1)
+#        implementation in PLY-1.x releases.   That version was based on
+#        a less efficient algorithm and it had bugs in its implementation.
+# -----------------------------------------------------------------------------
+
+# -----------------------------------------------------------------------------
+# compute_nullable_nonterminals()
+#
+# Creates a dictionary containing all of the non-terminals that might produce
+# an empty production.
+# -----------------------------------------------------------------------------
+
+def compute_nullable_nonterminals():
+    nullable = {}
+    num_nullable = 0
+    while 1:
+       for p in Productions[1:]:
+           if p.len == 0:
+                nullable[p.name] = 1
+                continue
+           for t in p.prod:
+                if not nullable.has_key(t): break
+           else:
+                nullable[p.name] = 1
+       if len(nullable) == num_nullable: break
+       num_nullable = len(nullable)
+    return nullable
+
+# -----------------------------------------------------------------------------
+# find_nonterminal_trans(C)
+#
+# Given a set of LR(0) items, this functions finds all of the non-terminal
+# transitions.    These are transitions in which a dot appears immediately before
+# a non-terminal.   Returns a list of tuples of the form (state,N) where state
+# is the state number and N is the nonterminal symbol.
+#
+# The input C is the set of LR(0) items.
+# -----------------------------------------------------------------------------
+
+def find_nonterminal_transitions(C):
+     trans = []
+     for state in range(len(C)):
+         for p in C[state]:
+             if p.lr_index < p.len - 1:
+                  t = (state,p.prod[p.lr_index+1])
+                  if Nonterminals.has_key(t[1]):
+                        if t not in trans: trans.append(t)
+         state = state + 1
+     return trans
+
+# -----------------------------------------------------------------------------
+# dr_relation()
+#
+# Computes the DR(p,A) relationships for non-terminal transitions.  The input
+# is a tuple (state,N) where state is a number and N is a nonterminal symbol.
+#
+# Returns a list of terminals.
+# -----------------------------------------------------------------------------
+
+def dr_relation(C,trans,nullable):
+    dr_set = { }
+    state,N = trans
+    terms = []
+
+    g = lr0_goto(C[state],N)
+    for p in g:
+       if p.lr_index < p.len - 1:
+           a = p.prod[p.lr_index+1]
+           if Terminals.has_key(a):
+               if a not in terms: terms.append(a)
+
+    # This extra bit is to handle the start state
+    if state == 0 and N == Productions[0].prod[0]:
+       terms.append('$end')
+
+    return terms
+
+# -----------------------------------------------------------------------------
+# reads_relation()
+#
+# Computes the READS() relation (p,A) READS (t,C).
+# -----------------------------------------------------------------------------
+
+def reads_relation(C, trans, empty):
+    # Look for empty transitions
+    rel = []
+    state, N = trans
+
+    g = lr0_goto(C[state],N)
+    j = _lr0_cidhash.get(id(g),-1)
+    for p in g:
+        if p.lr_index < p.len - 1:
+             a = p.prod[p.lr_index + 1]
+             if empty.has_key(a):
+                  rel.append((j,a))
+
+    return rel
+
+# -----------------------------------------------------------------------------
+# compute_lookback_includes()
+#
+# Determines the lookback and includes relations
+#
+# LOOKBACK:
+#
+# This relation is determined by running the LR(0) state machine forward.
+# For example, starting with a production "N : . A B C", we run it forward
+# to obtain "N : A B C ."   We then build a relationship between this final
+# state and the starting state.   These relationships are stored in a dictionary
+# lookdict.
+#
+# INCLUDES:
+#
+# Computes the INCLUDE() relation (p,A) INCLUDES (p',B).
+#
+# This relation is used to determine non-terminal transitions that occur
+# inside of other non-terminal transition states.   (p,A) INCLUDES (p', B)
+# if the following holds:
+#
+#       B -> LAT, where T -> epsilon and p' -L-> p
+#
+# L is essentially a prefix (which may be empty), T is a suffix that must be
+# able to derive an empty string.  State p' must lead to state p with the string L.
+#
+# -----------------------------------------------------------------------------
+
+def compute_lookback_includes(C,trans,nullable):
+
+    lookdict = {}          # Dictionary of lookback relations
+    includedict = {}       # Dictionary of include relations
+
+    # Make a dictionary of non-terminal transitions
+    dtrans = {}
+    for t in trans:
+        dtrans[t] = 1
+
+    # Loop over all transitions and compute lookbacks and includes
+    for state,N in trans:
+        lookb = []
+        includes = []
+        for p in C[state]:
+            if p.name != N: continue
+
+            # Okay, we have a name match.  We now follow the production all the way
+            # through the state machine until we get the . on the right hand side
+
+            lr_index = p.lr_index
+            j = state
+            while lr_index < p.len - 1:
+                 lr_index = lr_index + 1
+                 t = p.prod[lr_index]
+
+                 # Check to see if this symbol and state are a non-terminal transition
+                 if dtrans.has_key((j,t)):
+                       # Yes.  Okay, there is some chance that this is an includes relation
+                       # the only way to know for certain is whether the rest of the
+                       # production derives empty
+
+                       li = lr_index + 1
+                       while li < p.len:
+                            if Terminals.has_key(p.prod[li]): break      # No forget it
+                            if not nullable.has_key(p.prod[li]): break
+                            li = li + 1
+                       else:
+                            # Appears to be a relation between (j,t) and (state,N)
+                            includes.append((j,t))
+
+                 g = lr0_goto(C[j],t)               # Go to next set
+                 j = _lr0_cidhash.get(id(g),-1)     # Go to next state
+
+            # When we get here, j is the final state, now we have to locate the production
+            for r in C[j]:
+                 if r.name != p.name: continue
+                 if r.len != p.len:   continue
+                 i = 0
+                 # This look is comparing a production ". A B C" with "A B C ."
+                 while i < r.lr_index:
+                      if r.prod[i] != p.prod[i+1]: break
+                      i = i + 1
+                 else:
+                      lookb.append((j,r))
+        for i in includes:
+             if not includedict.has_key(i): includedict[i] = []
+             includedict[i].append((state,N))
+        lookdict[(state,N)] = lookb
+
+    return lookdict,includedict
+
+# -----------------------------------------------------------------------------
+# digraph()
+# traverse()
+#
+# The following two functions are used to compute set valued functions
+# of the form:
+#
+#     F(x) = F'(x) U U{F(y) | x R y}
+#
+# This is used to compute the values of Read() sets as well as FOLLOW sets
+# in LALR(1) generation.
+#
+# Inputs:  X    - An input set
+#          R    - A relation
+#          FP   - Set-valued function
+# ------------------------------------------------------------------------------
+
+def digraph(X,R,FP):
+    N = { }
+    for x in X:
+       N[x] = 0
+    stack = []
+    F = { }
+    for x in X:
+        if N[x] == 0: traverse(x,N,stack,F,X,R,FP)
+    return F
+
+def traverse(x,N,stack,F,X,R,FP):
+    stack.append(x)
+    d = len(stack)
+    N[x] = d
+    F[x] = FP(x)             # F(X) <- F'(x)
+
+    rel = R(x)               # Get y's related to x
+    for y in rel:
+        if N[y] == 0:
+             traverse(y,N,stack,F,X,R,FP)
+        N[x] = min(N[x],N[y])
+        for a in F.get(y,[]):
+            if a not in F[x]: F[x].append(a)
+    if N[x] == d:
+       N[stack[-1]] = sys.maxint
+       F[stack[-1]] = F[x]
+       element = stack.pop()
+       while element != x:
+           N[stack[-1]] = sys.maxint
+           F[stack[-1]] = F[x]
+           element = stack.pop()
+
+# -----------------------------------------------------------------------------
+# compute_read_sets()
+#
+# Given a set of LR(0) items, this function computes the read sets.
+#
+# Inputs:  C        =  Set of LR(0) items
+#          ntrans   = Set of nonterminal transitions
+#          nullable = Set of empty transitions
+#
+# Returns a set containing the read sets
+# -----------------------------------------------------------------------------
+
+def compute_read_sets(C, ntrans, nullable):
+    FP = lambda x: dr_relation(C,x,nullable)
+    R =  lambda x: reads_relation(C,x,nullable)
+    F = digraph(ntrans,R,FP)
+    return F
+
+# -----------------------------------------------------------------------------
+# compute_follow_sets()
+#
+# Given a set of LR(0) items, a set of non-terminal transitions, a readset,
+# and an include set, this function computes the follow sets
+#
+# Follow(p,A) = Read(p,A) U U {Follow(p',B) | (p,A) INCLUDES (p',B)}
+#
+# Inputs:
+#            ntrans     = Set of nonterminal transitions
+#            readsets   = Readset (previously computed)
+#            inclsets   = Include sets (previously computed)
+#
+# Returns a set containing the follow sets
+# -----------------------------------------------------------------------------
+
+def compute_follow_sets(ntrans,readsets,inclsets):
+     FP = lambda x: readsets[x]
+     R  = lambda x: inclsets.get(x,[])
+     F = digraph(ntrans,R,FP)
+     return F
+
+# -----------------------------------------------------------------------------
+# add_lookaheads()
+#
+# Attaches the lookahead symbols to grammar rules.
+#
+# Inputs:    lookbacks         -  Set of lookback relations
+#            followset         -  Computed follow set
+#
+# This function directly attaches the lookaheads to productions contained
+# in the lookbacks set
+# -----------------------------------------------------------------------------
+
+def add_lookaheads(lookbacks,followset):
+    for trans,lb in lookbacks.items():
+        # Loop over productions in lookback
+        for state,p in lb:
+             if not p.lookaheads.has_key(state):
+                  p.lookaheads[state] = []
+             f = followset.get(trans,[])
+             for a in f:
+                  if a not in p.lookaheads[state]: p.lookaheads[state].append(a)
+
+# -----------------------------------------------------------------------------
+# add_lalr_lookaheads()
+#
+# This function does all of the work of adding lookahead information for use
+# with LALR parsing
+# -----------------------------------------------------------------------------
+
+def add_lalr_lookaheads(C):
+    # Determine all of the nullable nonterminals
+    nullable = compute_nullable_nonterminals()
+
+    # Find all non-terminal transitions
+    trans = find_nonterminal_transitions(C)
+
+    # Compute read sets
+    readsets = compute_read_sets(C,trans,nullable)
+
+    # Compute lookback/includes relations
+    lookd, included = compute_lookback_includes(C,trans,nullable)
+
+    # Compute LALR FOLLOW sets
+    followsets = compute_follow_sets(trans,readsets,included)
+
+    # Add all of the lookaheads
+    add_lookaheads(lookd,followsets)
+
+# -----------------------------------------------------------------------------
+# lr_parse_table()
+#
+# This function constructs the parse tables for SLR or LALR
+# -----------------------------------------------------------------------------
+def lr_parse_table(method):
+    global _lr_method
+    goto = _lr_goto           # Goto array
+    action = _lr_action       # Action array
+    actionp = { }             # Action production array (temporary)
+
+    _lr_method = method
+
+    n_srconflict = 0
+    n_rrconflict = 0
+
+    if yaccdebug:
+        sys.stderr.write("yacc: Generating %s parsing table...\n" % method)
+        _vf.write("\n\nParsing method: %s\n\n" % method)
+
+    # Step 1: Construct C = { I0, I1, ... IN}, collection of LR(0) items
+    # This determines the number of states
+
+    C = lr0_items()
+
+    if method == 'LALR':
+        add_lalr_lookaheads(C)
+
+    # Build the parser table, state by state
+    st = 0
+    for I in C:
+        # Loop over each production in I
+        actlist = [ ]              # List of actions
+
+        if yaccdebug:
+            _vf.write("\nstate %d\n\n" % st)
+            for p in I:
+                _vf.write("    (%d) %s\n" % (p.number, str(p)))
+            _vf.write("\n")
+
+        for p in I:
+            try:
+                if p.prod[-1] == ".":
+                    if p.name == "S'":
+                        # Start symbol. Accept!
+                        action[st,"$end"] = 0
+                        actionp[st,"$end"] = p
+                    else:
+                        # We are at the end of a production.  Reduce!
+                        if method == 'LALR':
+                            laheads = p.lookaheads[st]
+                        else:
+                            laheads = Follow[p.name]
+                        for a in laheads:
+                            actlist.append((a,p,"reduce using rule %d (%s)" % (p.number,p)))
+                            r = action.get((st,a),None)
+                            if r is not None:
+                                # Whoa. Have a shift/reduce or reduce/reduce conflict
+                                if r > 0:
+                                    # Need to decide on shift or reduce here
+                                    # By default we favor shifting. Need to add
+                                    # some precedence rules here.
+                                    sprec,slevel = Productions[actionp[st,a].number].prec
+                                    rprec,rlevel = Precedence.get(a,('right',0))
+                                    if (slevel < rlevel) or ((slevel == rlevel) and (rprec == 'left')):
+                                        # We really need to reduce here.
+                                        action[st,a] = -p.number
+                                        actionp[st,a] = p
+                                        if not slevel and not rlevel:
+                                            _vfc.write("shift/reduce conflict in state %d resolved as reduce.\n" % st)
+                                            _vf.write("  ! shift/reduce conflict for %s resolved as reduce.\n" % a)
+                                            n_srconflict += 1
+                                    elif (slevel == rlevel) and (rprec == 'nonassoc'):
+                                        action[st,a] = None
+                                    else:
+                                        # Hmmm. Guess we'll keep the shift
+                                        if not rlevel:
+                                            _vfc.write("shift/reduce conflict in state %d resolved as shift.\n" % st)
+                                            _vf.write("  ! shift/reduce conflict for %s resolved as shift.\n" % a)
+                                            n_srconflict +=1
+                                elif r < 0:
+                                    # Reduce/reduce conflict.   In this case, we favor the rule
+                                    # that was defined first in the grammar file
+                                    oldp = Productions[-r]
+                                    pp = Productions[p.number]
+                                    if oldp.line > pp.line:
+                                        action[st,a] = -p.number
+                                        actionp[st,a] = p
+                                    # sys.stderr.write("Reduce/reduce conflict in state %d\n" % st)
+                                    n_rrconflict += 1
+                                    _vfc.write("reduce/reduce conflict in state %d resolved using rule %d (%s).\n" % (st, actionp[st,a].number, actionp[st,a]))
+                                    _vf.write("  ! reduce/reduce conflict for %s resolved using rule %d (%s).\n" % (a,actionp[st,a].number, actionp[st,a]))
+                                else:
+                                    sys.stderr.write("Unknown conflict in state %d\n" % st)
+                            else:
+                                action[st,a] = -p.number
+                                actionp[st,a] = p
+                else:
+                    i = p.lr_index
+                    a = p.prod[i+1]       # Get symbol right after the "."
+                    if Terminals.has_key(a):
+                        g = lr0_goto(I,a)
+                        j = _lr0_cidhash.get(id(g),-1)
+                        if j >= 0:
+                            # We are in a shift state
+                            actlist.append((a,p,"shift and go to state %d" % j))
+                            r = action.get((st,a),None)
+                            if r is not None:
+                                # Whoa have a shift/reduce or shift/shift conflict
+                                if r > 0:
+                                    if r != j:
+                                        sys.stderr.write("Shift/shift conflict in state %d\n" % st)
+                                elif r < 0:
+                                    # Do a precedence check.
+                                    #   -  if precedence of reduce rule is higher, we reduce.
+                                    #   -  if precedence of reduce is same and left assoc, we reduce.
+                                    #   -  otherwise we shift
+                                    rprec,rlevel = Productions[actionp[st,a].number].prec
+                                    sprec,slevel = Precedence.get(a,('right',0))
+                                    if (slevel > rlevel) or ((slevel == rlevel) and (rprec != 'left')):
+                                        # We decide to shift here... highest precedence to shift
+                                        action[st,a] = j
+                                        actionp[st,a] = p
+                                        if not rlevel:
+                                            n_srconflict += 1
+                                            _vfc.write("shift/reduce conflict in state %d resolved as shift.\n" % st)
+                                            _vf.write("  ! shift/reduce conflict for %s resolved as shift.\n" % a)
+                                    elif (slevel == rlevel) and (rprec == 'nonassoc'):
+                                        action[st,a] = None
+                                    else:
+                                        # Hmmm. Guess we'll keep the reduce
+                                        if not slevel and not rlevel:
+                                            n_srconflict +=1
+                                            _vfc.write("shift/reduce conflict in state %d resolved as reduce.\n" % st)
+                                            _vf.write("  ! shift/reduce conflict for %s resolved as reduce.\n" % a)
+
+                                else:
+                                    sys.stderr.write("Unknown conflict in state %d\n" % st)
+                            else:
+                                action[st,a] = j
+                                actionp[st,a] = p
+
+            except Exception as e:
+                raise YaccError("Hosed in lr_parse_table", e)
+
+        # Print the actions associated with each terminal
+        if yaccdebug:
+          _actprint = { }
+          for a,p,m in actlist:
+            if action.has_key((st,a)):
+                if p is actionp[st,a]:
+                    _vf.write("    %-15s %s\n" % (a,m))
+                    _actprint[(a,m)] = 1
+          _vf.write("\n")
+          for a,p,m in actlist:
+            if action.has_key((st,a)):
+                if p is not actionp[st,a]:
+                    if not _actprint.has_key((a,m)):
+                        _vf.write("  ! %-15s [ %s ]\n" % (a,m))
+                        _actprint[(a,m)] = 1
+
+        # Construct the goto table for this state
+        if yaccdebug:
+            _vf.write("\n")
+        nkeys = { }
+        for ii in I:
+            for s in ii.usyms:
+                if Nonterminals.has_key(s):
+                    nkeys[s] = None
+        for n in nkeys.keys():
+            g = lr0_goto(I,n)
+            j = _lr0_cidhash.get(id(g),-1)
+            if j >= 0:
+                goto[st,n] = j
+                if yaccdebug:
+                    _vf.write("    %-30s shift and go to state %d\n" % (n,j))
+
+        st += 1
+
+    if yaccdebug:
+        if n_srconflict == 1:
+            sys.stderr.write("yacc: %d shift/reduce conflict\n" % n_srconflict)
+        if n_srconflict > 1:
+            sys.stderr.write("yacc: %d shift/reduce conflicts\n" % n_srconflict)
+        if n_rrconflict == 1:
+            sys.stderr.write("yacc: %d reduce/reduce conflict\n" % n_rrconflict)
+        if n_rrconflict > 1:
+            sys.stderr.write("yacc: %d reduce/reduce conflicts\n" % n_rrconflict)
+
+# -----------------------------------------------------------------------------
+#                          ==== LR Utility functions ====
+# -----------------------------------------------------------------------------
+
+# -----------------------------------------------------------------------------
+# _lr_write_tables()
+#
+# This function writes the LR parsing tables to a file
+# -----------------------------------------------------------------------------
+
+def lr_write_tables(modulename=tab_module,outputdir=''):
+    filename = os.path.join(outputdir,modulename) + ".py"
+    try:
+        f = open(filename,"w")
+
+        f.write("""
+# %s
+# This file is automatically generated. Do not edit.
+
+_lr_method = %s
+
+_lr_signature = %s
+""" % (filename, repr(_lr_method), repr(Signature.digest())))
+
+        # Change smaller to 0 to go back to original tables
+        smaller = 1
+
+        # Factor out names to try and make smaller
+        if smaller:
+            items = { }
+
+            for k,v in _lr_action.items():
+                i = items.get(k[1])
+                if not i:
+                    i = ([],[])
+                    items[k[1]] = i
+                i[0].append(k[0])
+                i[1].append(v)
+
+            f.write("\n_lr_action_items = {")
+            for k,v in items.items():
+                f.write("%r:([" % k)
+                for i in v[0]:
+                    f.write("%r," % i)
+                f.write("],[")
+                for i in v[1]:
+                    f.write("%r," % i)
+
+                f.write("]),")
+            f.write("}\n")
+
+            f.write("""
+_lr_action = { }
+for _k, _v in _lr_action_items.items():
+   for _x,_y in zip(_v[0],_v[1]):
+       _lr_action[(_x,_k)] = _y
+del _lr_action_items
+""")
+
+        else:
+            f.write("\n_lr_action = { ");
+            for k,v in _lr_action.items():
+                f.write("(%r,%r):%r," % (k[0],k[1],v))
+            f.write("}\n");
+
+        if smaller:
+            # Factor out names to try and make smaller
+            items = { }
+
+            for k,v in _lr_goto.items():
+                i = items.get(k[1])
+                if not i:
+                    i = ([],[])
+                    items[k[1]] = i
+                i[0].append(k[0])
+                i[1].append(v)
+
+            f.write("\n_lr_goto_items = {")
+            for k,v in items.items():
+                f.write("%r:([" % k)
+                for i in v[0]:
+                    f.write("%r," % i)
+                f.write("],[")
+                for i in v[1]:
+                    f.write("%r," % i)
+
+                f.write("]),")
+            f.write("}\n")
+
+            f.write("""
+_lr_goto = { }
+for _k, _v in _lr_goto_items.items():
+   for _x,_y in zip(_v[0],_v[1]):
+       _lr_goto[(_x,_k)] = _y
+del _lr_goto_items
+""")
+        else:
+            f.write("\n_lr_goto = { ");
+            for k,v in _lr_goto.items():
+                f.write("(%r,%r):%r," % (k[0],k[1],v))
+            f.write("}\n");
+
+        # Write production table
+        f.write("_lr_productions = [\n")
+        for p in Productions:
+            if p:
+                if (p.func):
+                    f.write("  (%r,%d,%r,%r,%d),\n" % (p.name, p.len, p.func.__name__,p.file,p.line))
+                else:
+                    f.write("  (%r,%d,None,None,None),\n" % (p.name, p.len))
+            else:
+                f.write("  None,\n")
+        f.write("]\n")
+
+        f.close()
+
+    except IOError as e:
+        print("Unable to create '%s'" % filename)
+        print(e)
+        return
+
+def lr_read_tables(module=tab_module,optimize=0):
+    global _lr_action, _lr_goto, _lr_productions, _lr_method
+    try:
+        exec("import %s as parsetab" % module)
+        global parsetab  # declare the name of the imported module
+
+        if (optimize) or (Signature.digest() == parsetab._lr_signature):
+            _lr_action = parsetab._lr_action
+            _lr_goto   = parsetab._lr_goto
+            _lr_productions = parsetab._lr_productions
+            _lr_method = parsetab._lr_method
+            return 1
+        else:
+            return 0
+
+    except (ImportError,AttributeError):
+        return 0
+
+
+# Available instance types.  This is used when parsers are defined by a class.
+# it's a little funky because I want to preserve backwards compatibility
+# with Python 2.0 where types.ObjectType is undefined.
+
+try:
+   _INSTANCETYPE = (types.InstanceType, types.ObjectType)
+except AttributeError:
+   _INSTANCETYPE = types.InstanceType
+
+# -----------------------------------------------------------------------------
+# yacc(module)
+#
+# Build the parser module
+# -----------------------------------------------------------------------------
+
+# <ah> Add parserclass parameter.
+def yacc(method=default_lr, debug=yaccdebug, module=None, tabmodule=tab_module, start=None, check_recursion=1, optimize=0,write_tables=1,debugfile=debug_file,outputdir='', parserclass=Parser):
+    global yaccdebug
+    yaccdebug = debug
+
+    initialize_vars()
+    files = { }
+    error = 0
+
+
+    # Add parsing method to signature
+    Signature.update(method)
+
+    # If a "module" parameter was supplied, extract its dictionary.
+    # Note: a module may in fact be an instance as well.
+
+    if module:
+        # User supplied a module object.
+        if isinstance(module, types.ModuleType):
+            ldict = module.__dict__
+        elif isinstance(module, _INSTANCETYPE):
+            _items = [(k,getattr(module,k)) for k in dir(module)]
+            ldict = { }
+            for i in _items:
+                ldict[i[0]] = i[1]
+        else:
+            raise ValueError("Expected a module")
+
+    else:
+        # No module given.  We might be able to get information from the caller.
+        # Throw an exception and unwind the traceback to get the globals
+
+        try:
+            raise RuntimeError
+        except RuntimeError:
+            e,b,t = sys.exc_info()
+            f = t.tb_frame
+            f = f.f_back           # Walk out to our calling function
+            ldict = f.f_globals    # Grab its globals dictionary
+
+    # Add starting symbol to signature
+    if not start:
+        start = ldict.get("start",None)
+    if start:
+        Signature.update(start)
+
+    # If running in optimized mode.  We're going to
+
+    if (optimize and lr_read_tables(tabmodule,1)):
+        # Read parse table
+        del Productions[:]
+        for p in _lr_productions:
+            if not p:
+                Productions.append(None)
+            else:
+                m = MiniProduction()
+                m.name = p[0]
+                m.len  = p[1]
+                m.file = p[3]
+                m.line = p[4]
+                if p[2]:
+                    m.func = ldict[p[2]]
+                Productions.append(m)
+
+    else:
+        # Get the tokens map
+        if (module and isinstance(module,_INSTANCETYPE)):
+            tokens = getattr(module,"tokens",None)
+        else:
+            tokens = ldict.get("tokens",None)
+
+        if not tokens:
+            raise YaccError("module does not define a list 'tokens'")
+        if not (isinstance(tokens,types.ListType) or isinstance(tokens,types.TupleType)):
+            raise YaccError("tokens must be a list or tuple.")
+
+        # Check to see if a requires dictionary is defined.
+        requires = ldict.get("require",None)
+        if requires:
+            if not (isinstance(requires,types.DictType)):
+                raise YaccError("require must be a dictionary.")
+
+            for r,v in requires.items():
+                try:
+                    if not (isinstance(v,types.ListType)):
+                        raise TypeError
+                    v1 = [x.split(".") for x in v]
+                    Requires[r] = v1
+                except Exception:
+                    print("Invalid specification for rule '%s' in require. Expected a list of strings" % r)
+
+
+        # Build the dictionary of terminals.  We a record a 0 in the
+        # dictionary to track whether or not a terminal is actually
+        # used in the grammar
+
+        if 'error' in tokens:
+            print("yacc: Illegal token 'error'.  Is a reserved word.")
+            raise YaccError("Illegal token name")
+
+        for n in tokens:
+            if Terminals.has_key(n):
+                print("yacc: Warning. Token '%s' multiply defined." % n)
+            Terminals[n] = [ ]
+
+        Terminals['error'] = [ ]
+
+        # Get the precedence map (if any)
+        prec = ldict.get("precedence",None)
+        if prec:
+            if not (isinstance(prec,types.ListType) or isinstance(prec,types.TupleType)):
+                raise YaccError("precedence must be a list or tuple.")
+            add_precedence(prec)
+            Signature.update(repr(prec))
+
+        for n in tokens:
+            if not Precedence.has_key(n):
+                Precedence[n] = ('right',0)         # Default, right associative, 0 precedence
+
+        # Look for error handler
+        ef = ldict.get('p_error',None)
+        if ef:
+            if isinstance(ef,types.FunctionType):
+                ismethod = 0
+            elif isinstance(ef, types.MethodType):
+                ismethod = 1
+            else:
+                raise YaccError("'p_error' defined, but is not a function or method.")
+            eline = ef.func_code.co_firstlineno
+            efile = ef.func_code.co_filename
+            files[efile] = None
+
+            if (ef.func_code.co_argcount != 1+ismethod):
+                raise YaccError("%s:%d: p_error() requires 1 argument." % (efile,eline))
+            global Errorfunc
+            Errorfunc = ef
+        else:
+            print("yacc: Warning. no p_error() function is defined.")
+
+        # Get the list of built-in functions with p_ prefix
+        symbols = [ldict[f] for f in ldict.keys()
+               if (type(ldict[f]) in (types.FunctionType, types.MethodType) and ldict[f].__name__[:2] == 'p_'
+                   and ldict[f].__name__ != 'p_error')]
+
+        # Check for non-empty symbols
+        if len(symbols) == 0:
+            raise YaccError("no rules of the form p_rulename are defined.")
+
+        # Sort the symbols by line number
+        symbols.sort(key=lambda func: func.__code__.co_firstlineno)
+
+        # Add all of the symbols to the grammar
+        for f in symbols:
+            if (add_function(f)) < 0:
+                error += 1
+            else:
+                files[f.func_code.co_filename] = None
+
+        # Make a signature of the docstrings
+        for f in symbols:
+            if f.__doc__:
+                Signature.update(f.__doc__)
+
+        lr_init_vars()
+
+        if error:
+            raise YaccError("Unable to construct parser.")
+
+        if not lr_read_tables(tabmodule):
+
+            # Validate files
+            for filename in files.keys():
+                if not validate_file(filename):
+                    error = 1
+
+            # Validate dictionary
+            validate_dict(ldict)
+
+            if start and not Prodnames.has_key(start):
+                raise YaccError("Bad starting symbol '%s'" % start)
+
+            augment_grammar(start)
+            error = verify_productions(cycle_check=check_recursion)
+            otherfunc = [ldict[f] for f in ldict.keys()
+               if (type(f) in (types.FunctionType,types.MethodType) and ldict[f].__name__[:2] != 'p_')]
+
+            if error:
+                raise YaccError("Unable to construct parser.")
+
+            build_lritems()
+            compute_first1()
+            compute_follow(start)
+
+            if method in ['SLR','LALR']:
+                lr_parse_table(method)
+            else:
+                raise YaccError("Unknown parsing method '%s'" % method)
+
+            if write_tables:
+                lr_write_tables(tabmodule,outputdir)
+
+            if yaccdebug:
+                try:
+                    f = open(os.path.join(outputdir,debugfile),"w")
+                    f.write(_vfc.getvalue())
+                    f.write("\n\n")
+                    f.write(_vf.getvalue())
+                    f.close()
+                except IOError as e:
+                    print("yacc: can't create '%s'" % debugfile,e)
+
+    # Made it here.   Create a parser object and set up its internal state.
+    # Set global parse() method to bound method of parser object.
+
+    g = ParserPrototype("xyzzy")
+    g.productions = Productions
+    g.errorfunc = Errorfunc
+    g.action = _lr_action
+    g.goto   = _lr_goto
+    g.method = _lr_method
+    g.require = Requires
+
+    global parser
+    parser = g.init_parser()
+
+    global parse
+    parse = parser.parse
+
+    # Clean up all of the globals we created
+    if (not optimize):
+        yacc_cleanup()
+    return g
+
+# <ah> Allow multiple instances of parser
+class ParserPrototype(object):
+    def __init__(self, magic=None):
+        if magic != "xyzzy":
+            raise YaccError('Use yacc()')
+
+    def init_parser(self, parser=None):
+        if not parser:
+            parser = Parser()
+        parser.productions = self.productions
+        parser.errorfunc = self.errorfunc
+        parser.action = self.action
+        parser.goto   = self.goto
+        parser.method = self.method
+        parser.require = self.require
+        return parser
+
+# yacc_cleanup function.  Delete all of the global variables
+# used during table construction
+
+def yacc_cleanup():
+    global _lr_action, _lr_goto, _lr_method, _lr_goto_cache
+    del _lr_action, _lr_goto, _lr_method, _lr_goto_cache
+
+    global Productions, Prodnames, Prodmap, Terminals
+    global Nonterminals, First, Follow, Precedence, LRitems
+    global Errorfunc, Signature, Requires
+
+    del Productions, Prodnames, Prodmap, Terminals
+    del Nonterminals, First, Follow, Precedence, LRitems
+    del Errorfunc, Signature, Requires
+
+    global _vf, _vfc
+    del _vf, _vfc
+
+
+# Stub that raises an error if parsing is attempted without first calling yacc()
+def parse(*args,**kwargs):
+    raise YaccError("yacc: No parser built with yacc()")
+
diff --git a/website/LICENSE.txt b/website/LICENSE.txt
new file mode 100644
index 0000000..e100aff
--- /dev/null
+++ b/website/LICENSE.txt
@@ -0,0 +1,427 @@
+Attribution-ShareAlike 4.0 International
+
+=======================================================================
+
+Creative Commons Corporation ("Creative Commons") is not a law firm and
+does not provide legal services or legal advice. Distribution of
+Creative Commons public licenses does not create a lawyer-client or
+other relationship. Creative Commons makes its licenses and related
+information available on an "as-is" basis. Creative Commons gives no
+warranties regarding its licenses, any material licensed under their
+terms and conditions, or any related information. Creative Commons
+disclaims all liability for damages resulting from their use to the
+fullest extent possible.
+
+Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and
+conditions that creators and other rights holders may use to share
+original works of authorship and other material subject to copyright
+and certain other rights specified in the public license below. The
+following considerations are for informational purposes only, are not
+exhaustive, and do not form part of our licenses.
+
+     Considerations for licensors: Our public licenses are
+     intended for use by those authorized to give the public
+     permission to use material in ways otherwise restricted by
+     copyright and certain other rights. Our licenses are
+     irrevocable. Licensors should read and understand the terms
+     and conditions of the license they choose before applying it.
+     Licensors should also secure all rights necessary before
+     applying our licenses so that the public can reuse the
+     material as expected. Licensors should clearly mark any
+     material not subject to the license. This includes other CC-
+     licensed material, or material used under an exception or
+     limitation to copyright. More considerations for licensors:
+     wiki.creativecommons.org/Considerations_for_licensors
+
+     Considerations for the public: By using one of our public
+     licenses, a licensor grants the public permission to use the
+     licensed material under specified terms and conditions. If
+     the licensor's permission is not necessary for any reason--for
+     example, because of any applicable exception or limitation to
+     copyright--then that use is not regulated by the license. Our
+     licenses grant only permissions under copyright and certain
+     other rights that a licensor has authority to grant. Use of
+     the licensed material may still be restricted for other
+     reasons, including because others have copyright or other
+     rights in the material. A licensor may make special requests,
+     such as asking that all changes be marked or described.
+     Although not required by our licenses, you are encouraged to
+     respect those requests where reasonable. More_considerations
+     for the public:
+     wiki.creativecommons.org/Considerations_for_licensees
+
+=======================================================================
+
+Creative Commons Attribution-ShareAlike 4.0 International Public
+License
+
+By exercising the Licensed Rights (defined below), You accept and agree
+to be bound by the terms and conditions of this Creative Commons
+Attribution-ShareAlike 4.0 International Public License ("Public
+License"). To the extent this Public License may be interpreted as a
+contract, You are granted the Licensed Rights in consideration of Your
+acceptance of these terms and conditions, and the Licensor grants You
+such rights in consideration of benefits the Licensor receives from
+making the Licensed Material available under these terms and
+conditions.
+
+
+Section 1 -- Definitions.
+
+  a. Adapted Material means material subject to Copyright and Similar
+     Rights that is derived from or based upon the Licensed Material
+     and in which the Licensed Material is translated, altered,
+     arranged, transformed, or otherwise modified in a manner requiring
+     permission under the Copyright and Similar Rights held by the
+     Licensor. For purposes of this Public License, where the Licensed
+     Material is a musical work, performance, or sound recording,
+     Adapted Material is always produced where the Licensed Material is
+     synched in timed relation with a moving image.
+
+  b. Adapter's License means the license You apply to Your Copyright
+     and Similar Rights in Your contributions to Adapted Material in
+     accordance with the terms and conditions of this Public License.
+
+  c. BY-SA Compatible License means a license listed at
+     creativecommons.org/compatiblelicenses, approved by Creative
+     Commons as essentially the equivalent of this Public License.
+
+  d. Copyright and Similar Rights means copyright and/or similar rights
+     closely related to copyright including, without limitation,
+     performance, broadcast, sound recording, and Sui Generis Database
+     Rights, without regard to how the rights are labeled or
+     categorized. For purposes of this Public License, the rights
+     specified in Section 2(b)(1)-(2) are not Copyright and Similar
+     Rights.
+
+  e. Effective Technological Measures means those measures that, in the
+     absence of proper authority, may not be circumvented under laws
+     fulfilling obligations under Article 11 of the WIPO Copyright
+     Treaty adopted on December 20, 1996, and/or similar international
+     agreements.
+
+  f. Exceptions and Limitations means fair use, fair dealing, and/or
+     any other exception or limitation to Copyright and Similar Rights
+     that applies to Your use of the Licensed Material.
+
+  g. License Elements means the license attributes listed in the name
+     of a Creative Commons Public License. The License Elements of this
+     Public License are Attribution and ShareAlike.
+
+  h. Licensed Material means the artistic or literary work, database,
+     or other material to which the Licensor applied this Public
+     License.
+
+  i. Licensed Rights means the rights granted to You subject to the
+     terms and conditions of this Public License, which are limited to
+     all Copyright and Similar Rights that apply to Your use of the
+     Licensed Material and that the Licensor has authority to license.
+
+  j. Licensor means the individual(s) or entity(ies) granting rights
+     under this Public License.
+
+  k. Share means to provide material to the public by any means or
+     process that requires permission under the Licensed Rights, such
+     as reproduction, public display, public performance, distribution,
+     dissemination, communication, or importation, and to make material
+     available to the public including in ways that members of the
+     public may access the material from a place and at a time
+     individually chosen by them.
+
+  l. Sui Generis Database Rights means rights other than copyright
+     resulting from Directive 96/9/EC of the European Parliament and of
+     the Council of 11 March 1996 on the legal protection of databases,
+     as amended and/or succeeded, as well as other essentially
+     equivalent rights anywhere in the world.
+
+  m. You means the individual or entity exercising the Licensed Rights
+     under this Public License. Your has a corresponding meaning.
+
+
+Section 2 -- Scope.
+
+  a. License grant.
+
+       1. Subject to the terms and conditions of this Public License,
+          the Licensor hereby grants You a worldwide, royalty-free,
+          non-sublicensable, non-exclusive, irrevocable license to
+          exercise the Licensed Rights in the Licensed Material to:
+
+            a. reproduce and Share the Licensed Material, in whole or
+               in part; and
+
+            b. produce, reproduce, and Share Adapted Material.
+
+       2. Exceptions and Limitations. For the avoidance of doubt, where
+          Exceptions and Limitations apply to Your use, this Public
+          License does not apply, and You do not need to comply with
+          its terms and conditions.
+
+       3. Term. The term of this Public License is specified in Section
+          6(a).
+
+       4. Media and formats; technical modifications allowed. The
+          Licensor authorizes You to exercise the Licensed Rights in
+          all media and formats whether now known or hereafter created,
+          and to make technical modifications necessary to do so. The
+          Licensor waives and/or agrees not to assert any right or
+          authority to forbid You from making technical modifications
+          necessary to exercise the Licensed Rights, including
+          technical modifications necessary to circumvent Effective
+          Technological Measures. For purposes of this Public License,
+          simply making modifications authorized by this Section 2(a)
+          (4) never produces Adapted Material.
+
+       5. Downstream recipients.
+
+            a. Offer from the Licensor -- Licensed Material. Every
+               recipient of the Licensed Material automatically
+               receives an offer from the Licensor to exercise the
+               Licensed Rights under the terms and conditions of this
+               Public License.
+
+            b. Additional offer from the Licensor -- Adapted Material.
+               Every recipient of Adapted Material from You
+               automatically receives an offer from the Licensor to
+               exercise the Licensed Rights in the Adapted Material
+               under the conditions of the Adapter's License You apply.
+
+            c. No downstream restrictions. You may not offer or impose
+               any additional or different terms or conditions on, or
+               apply any Effective Technological Measures to, the
+               Licensed Material if doing so restricts exercise of the
+               Licensed Rights by any recipient of the Licensed
+               Material.
+
+       6. No endorsement. Nothing in this Public License constitutes or
+          may be construed as permission to assert or imply that You
+          are, or that Your use of the Licensed Material is, connected
+          with, or sponsored, endorsed, or granted official status by,
+          the Licensor or others designated to receive attribution as
+          provided in Section 3(a)(1)(A)(i).
+
+  b. Other rights.
+
+       1. Moral rights, such as the right of integrity, are not
+          licensed under this Public License, nor are publicity,
+          privacy, and/or other similar personality rights; however, to
+          the extent possible, the Licensor waives and/or agrees not to
+          assert any such rights held by the Licensor to the limited
+          extent necessary to allow You to exercise the Licensed
+          Rights, but not otherwise.
+
+       2. Patent and trademark rights are not licensed under this
+          Public License.
+
+       3. To the extent possible, the Licensor waives any right to
+          collect royalties from You for the exercise of the Licensed
+          Rights, whether directly or through a collecting society
+          under any voluntary or waivable statutory or compulsory
+          licensing scheme. In all other cases the Licensor expressly
+          reserves any right to collect such royalties.
+
+
+Section 3 -- License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the
+following conditions.
+
+  a. Attribution.
+
+       1. If You Share the Licensed Material (including in modified
+          form), You must:
+
+            a. retain the following if it is supplied by the Licensor
+               with the Licensed Material:
+
+                 i. identification of the creator(s) of the Licensed
+                    Material and any others designated to receive
+                    attribution, in any reasonable manner requested by
+                    the Licensor (including by pseudonym if
+                    designated);
+
+                ii. a copyright notice;
+
+               iii. a notice that refers to this Public License;
+
+                iv. a notice that refers to the disclaimer of
+                    warranties;
+
+                 v. a URI or hyperlink to the Licensed Material to the
+                    extent reasonably practicable;
+
+            b. indicate if You modified the Licensed Material and
+               retain an indication of any previous modifications; and
+
+            c. indicate the Licensed Material is licensed under this
+               Public License, and include the text of, or the URI or
+               hyperlink to, this Public License.
+
+       2. You may satisfy the conditions in Section 3(a)(1) in any
+          reasonable manner based on the medium, means, and context in
+          which You Share the Licensed Material. For example, it may be
+          reasonable to satisfy the conditions by providing a URI or
+          hyperlink to a resource that includes the required
+          information.
+
+       3. If requested by the Licensor, You must remove any of the
+          information required by Section 3(a)(1)(A) to the extent
+          reasonably practicable.
+
+  b. ShareAlike.
+
+     In addition to the conditions in Section 3(a), if You Share
+     Adapted Material You produce, the following conditions also apply.
+
+       1. The Adapter's License You apply must be a Creative Commons
+          license with the same License Elements, this version or
+          later, or a BY-SA Compatible License.
+
+       2. You must include the text of, or the URI or hyperlink to, the
+          Adapter's License You apply. You may satisfy this condition
+          in any reasonable manner based on the medium, means, and
+          context in which You Share Adapted Material.
+
+       3. You may not offer or impose any additional or different terms
+          or conditions on, or apply any Effective Technological
+          Measures to, Adapted Material that restrict exercise of the
+          rights granted under the Adapter's License You apply.
+
+
+Section 4 -- Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that
+apply to Your use of the Licensed Material:
+
+  a. for the avoidance of doubt, Section 2(a)(1) grants You the right
+     to extract, reuse, reproduce, and Share all or a substantial
+     portion of the contents of the database;
+
+  b. if You include all or a substantial portion of the database
+     contents in a database in which You have Sui Generis Database
+     Rights, then the database in which You have Sui Generis Database
+     Rights (but not its individual contents) is Adapted Material,
+
+     including for purposes of Section 3(b); and
+  c. You must comply with the conditions in Section 3(a) if You Share
+     all or a substantial portion of the contents of the database.
+
+For the avoidance of doubt, this Section 4 supplements and does not
+replace Your obligations under this Public License where the Licensed
+Rights include other Copyright and Similar Rights.
+
+
+Section 5 -- Disclaimer of Warranties and Limitation of Liability.
+
+  a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
+     EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
+     AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
+     ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
+     IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
+     WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
+     PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
+     ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
+     KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
+     ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
+
+  b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
+     TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
+     NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
+     INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
+     COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
+     USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
+     ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
+     DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
+     IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
+
+  c. The disclaimer of warranties and limitation of liability provided
+     above shall be interpreted in a manner that, to the extent
+     possible, most closely approximates an absolute disclaimer and
+     waiver of all liability.
+
+
+Section 6 -- Term and Termination.
+
+  a. This Public License applies for the term of the Copyright and
+     Similar Rights licensed here. However, if You fail to comply with
+     this Public License, then Your rights under this Public License
+     terminate automatically.
+
+  b. Where Your right to use the Licensed Material has terminated under
+     Section 6(a), it reinstates:
+
+       1. automatically as of the date the violation is cured, provided
+          it is cured within 30 days of Your discovery of the
+          violation; or
+
+       2. upon express reinstatement by the Licensor.
+
+     For the avoidance of doubt, this Section 6(b) does not affect any
+     right the Licensor may have to seek remedies for Your violations
+     of this Public License.
+
+  c. For the avoidance of doubt, the Licensor may also offer the
+     Licensed Material under separate terms or conditions or stop
+     distributing the Licensed Material at any time; however, doing so
+     will not terminate this Public License.
+
+  d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
+     License.
+
+
+Section 7 -- Other Terms and Conditions.
+
+  a. The Licensor shall not be bound by any additional or different
+     terms or conditions communicated by You unless expressly agreed.
+
+  b. Any arrangements, understandings, or agreements regarding the
+     Licensed Material not stated herein are separate from and
+     independent of the terms and conditions of this Public License.
+
+
+Section 8 -- Interpretation.
+
+  a. For the avoidance of doubt, this Public License does not, and
+     shall not be interpreted to, reduce, limit, restrict, or impose
+     conditions on any use of the Licensed Material that could lawfully
+     be made without permission under this Public License.
+
+  b. To the extent possible, if any provision of this Public License is
+     deemed unenforceable, it shall be automatically reformed to the
+     minimum extent necessary to make it enforceable. If the provision
+     cannot be reformed, it shall be severed from this Public License
+     without affecting the enforceability of the remaining terms and
+     conditions.
+
+  c. No term or condition of this Public License will be waived and no
+     failure to comply consented to unless expressly agreed to by the
+     Licensor.
+
+  d. Nothing in this Public License constitutes or may be interpreted
+     as a limitation upon, or waiver of, any privileges and immunities
+     that apply to the Licensor or You, including from the legal
+     processes of any jurisdiction or authority.
+
+
+=======================================================================
+
+Creative Commons is not a party to its public
+licenses. Notwithstanding, Creative Commons may elect to apply one of
+its public licenses to material it publishes and in those instances
+will be considered the “Licensor.” The text of the Creative Commons
+public licenses is dedicated to the public domain under the CC0 Public
+Domain Dedication. Except for the limited purpose of indicating that
+material is shared under a Creative Commons public license or as
+otherwise permitted by the Creative Commons policies published at
+creativecommons.org/policies, Creative Commons does not authorize the
+use of the trademark "Creative Commons" or any other trademark or logo
+of Creative Commons without its prior written consent including,
+without limitation, in connection with any unauthorized modifications
+to any of its public licenses or any other arrangements,
+understandings, or agreements concerning use of licensed material. For
+the avoidance of doubt, this paragraph does not form part of the
+public licenses.
+
+Creative Commons may be contacted at creativecommons.org.
diff --git a/website/README.rst b/website/README.rst
new file mode 100644
index 0000000..44bfcab
--- /dev/null
+++ b/website/README.rst
@@ -0,0 +1,20 @@
+pyglet website
+==============
+
+The pyglet website is a simple static site made with the *Lektor* CMS.
+Lektor allows you to create static sites, but provides the ability to
+edit them like a CMS. Lektor can be installed via pip::
+
+    pip install lektor --user
+
+To edit the site, first change to the "website" directory run::
+
+    lektor server
+
+Please see the documentation at https://getlektor.org for more information. 
+Alternatively, if you do not wish to install or use Lektor, you can edit
+the static **\*.lr** files directly.
+
+
+After changing the website, it will be built and deployed automatically
+when pushed to the repository.
\ No newline at end of file
diff --git a/website/assets/favicon.ico b/website/assets/favicon.ico
new file mode 100644
index 0000000..66965a5
Binary files /dev/null and b/website/assets/favicon.ico differ
diff --git a/website/assets/static/css/example-custom-styles.css b/website/assets/static/css/example-custom-styles.css
new file mode 100644
index 0000000..f1bbf54
--- /dev/null
+++ b/website/assets/static/css/example-custom-styles.css
@@ -0,0 +1,19 @@
+@charset "UTF-8";
+
+/* Site custom styles */
+
+.red-text-test {
+  color: red;
+}
+
+.hero-section .container .col-md-6 {
+  float: left;
+}
+
+.hero-section .js-fullheight-home h1 {
+  margin-top: 75px;
+}
+
+.hero-section .js-fullheight-home h2 {
+  color: #ffffff;
+}
diff --git a/website/assets/static/images/pyglet.png b/website/assets/static/images/pyglet.png
new file mode 100644
index 0000000..c8bbfc8
Binary files /dev/null and b/website/assets/static/images/pyglet.png differ
diff --git a/website/content/404.html/contents.lr b/website/content/404.html/contents.lr
new file mode 100644
index 0000000..fcffa01
--- /dev/null
+++ b/website/content/404.html/contents.lr
@@ -0,0 +1,15 @@
+_model: 404
+---
+_discoverable: false
+---
+full_title: 404 — Page Not Found
+---
+short_title: 404
+---
+bg_image: ngc-5793.jpg
+---
+bg_image_fadeout: 0.1
+---
+message: This page does not exist! Please click on the title bar to return home.
+---
+_hidden: no
diff --git a/website/content/404.html/ngc-5793.jpg b/website/content/404.html/ngc-5793.jpg
new file mode 100644
index 0000000..25f17ab
Binary files /dev/null and b/website/content/404.html/ngc-5793.jpg differ
diff --git a/website/content/authors/benjamin/contents.lr b/website/content/authors/benjamin/contents.lr
new file mode 100644
index 0000000..f017d19
--- /dev/null
+++ b/website/content/authors/benjamin/contents.lr
@@ -0,0 +1,3 @@
+name: Benjamin
+---
+image: pyglet.png
diff --git a/website/content/authors/benjamin/pyglet.png b/website/content/authors/benjamin/pyglet.png
new file mode 100644
index 0000000..c8bbfc8
Binary files /dev/null and b/website/content/authors/benjamin/pyglet.png differ
diff --git a/website/content/authors/contents.lr b/website/content/authors/contents.lr
new file mode 100644
index 0000000..8afa65c
--- /dev/null
+++ b/website/content/authors/contents.lr
@@ -0,0 +1,3 @@
+_model: authors
+---
+_discoverable: false
diff --git a/website/content/blog/contents.lr b/website/content/blog/contents.lr
new file mode 100644
index 0000000..138d30b
--- /dev/null
+++ b/website/content/blog/contents.lr
@@ -0,0 +1,7 @@
+_model: blog
+---
+sort_key: 1
+---
+title: Latest Blog Posts
+---
+allow_comments_default: true
diff --git a/website/content/blog/welcome-blog/contents.lr b/website/content/blog/welcome-blog/contents.lr
new file mode 100644
index 0000000..49ea616
--- /dev/null
+++ b/website/content/blog/welcome-blog/contents.lr
@@ -0,0 +1,19 @@
+title: Welcome to the new pyglet website!
+---
+pub_date: 2019-07-31 23:00
+---
+author: benjamin
+---
+category: Announcement
+---
+tags: announcement
+---
+summary: We are happy to welcome everyone to the new pyglet website!
+---
+body:
+
+We are happy to welcome everyone to the new pyglet website!
+
+After a very long hiatus, pyglet once again has a simple website to call it's own. We have gone without a site for a long time, using the Bitbucket landing page as a pseudo web site. While that was functional, it certainly wasn't ideal. This new simple website will finally replace that as our face on the web. It will also be a more friendly place for new users just finding pyglet, and serve as a central point for users looking for links to pyglet related content around the web. This includes the mailing list, our Discord server, and of course the GitHub repository. Speaking of that, the GitHub repository itself is also new, having recently migrated from Bitbucket and HG due to popular demand. Going forward, myself and the rest of the pyglet contributors are working hard to keep pyglet evolving and useful. Lots of exciting developments are on the horizon, and I would also like to take this opportunity to invite you to join us! 
+
+Thank you, and looking forward to seeing everyone on the mailing list, Discord channel, or Github!
diff --git a/website/content/contents.lr b/website/content/contents.lr
new file mode 100644
index 0000000..4b26217
--- /dev/null
+++ b/website/content/contents.lr
@@ -0,0 +1,79 @@
+_model: single-layout
+---
+show_home_nav: true
+---
+hero_title: pyglet
+---
+hero_description: Welcome to the website of the <b>pyglet</b> project!
+---
+hero_image: eclipse.jpg
+---
+starting_block_bg: light
+---
+main_content:
+
+#### content ####
+title: About
+----
+description: The cross-platform windowing and multimedia library for Python.
+----
+nav_label: About
+----
+section_id: overview
+----
+content:
+
+*pyglet* is a powerful, yet easy to use Python library for developing games and other visually-rich applications on Windows, Mac OS X and Linux. It supports windowing, user interface event handling, Joysticks, OpenGL graphics, loading images and videos, and playing sounds and music. All of this with a friendly Pythonic API, that's simple to learn and doesn't get in your way. 
+
+pyglet is provided under the BSD open-source license, allowing you to use it for both commercial and other open-source projects with very little restriction.
+
+Read the documentation at https://pyglet.readthedocs.io and visit us on:
+----
+button_content: Github
+----
+button_type: text
+----
+button_link: https://github.com/pyglet/pyglet
+#### services ####
+title: Features
+----
+description: 
+----
+nav_label: Features
+----
+section_id: features
+----
+video_url: 
+----
+services:
+
+##### service #####
+title: No external dependencies
+-----
+description:
+
+No external dependencies or installation requirements. For most application and game requirements, pyglet needs nothing else besides Python, simplifying distribution and installation. This makes it easy to package your project with freezers such as PyInstaller. 
+-----
+icon: flexible-design.png
+##### service #####
+title: Flexible native windowing
+-----
+description:
+
+pyglet provides real platform native windows, allowing you to take advantage of multiple windows and multi-monitor desktops. Fully aware of multi-monitor setups, you have control over how your application or game is displayed. Create multiple floating windows, or single  windows with control over which monitor they appear on, full screen or windowed. 
+-----
+icon: responsive-layout.png
+##### service #####
+title: Built-in support for images and audio
+-----
+description:
+
+pyglet contains built-in support for standard formats such as wav, png, bmp, dds, and others. If that's not sufficient, pyglet can optionally use FFmpeg to play back audio formats such as MP3, OGG/Vorbis and WMA, and video formats such as MPEG-2, H.264, WMV, or anything else that FFmpeg supports. 
+-----
+icon: 
+##### service #####
+title: Written in pure Python
+-----
+description: pyglet is written entirely in pure Python, and makes use of the standard library ctypes module to interface with system libraries. You can modify the codebase or make a contribution without any compilation steps, or knowledge of another language. Despite being pure Python, pyglet has excellent performance thanks to advanced batching and GPU rendering. You can easily draw thousands of sprites or animations.
+-----
+icon: 
diff --git a/website/content/eclipse.jpg b/website/content/eclipse.jpg
new file mode 100644
index 0000000..a7b367b
Binary files /dev/null and b/website/content/eclipse.jpg differ
diff --git a/website/pyglet.lektorproject b/website/pyglet.lektorproject
new file mode 100644
index 0000000..f8889ed
--- /dev/null
+++ b/website/pyglet.lektorproject
@@ -0,0 +1,29 @@
+[project]
+name = pyglet project
+themes = lektor-icon
+
+[servers.ghpages]
+target = ghpages+https://pyglet/pyglet.github.io?cname=pyglet.org
+
+
+[theme_settings]
+title = pyglet
+content_lang = en-US
+description = Web site of the pyglet project
+keywords = website, pyglet, python, gamedev, development
+favicon_path = /favicon.ico
+theme_accent_color = #ff6161
+theme_pipe_color = rgb(255, 76, 82)
+custom_css = /static/css/example-custom-styles.css
+
+content_security_policy = true
+content_security_policy_frame_src =
+content_security_policy_script_src =
+content_security_policy_style_src =
+
+loader_enable = true
+nav_logo_path = /static/images/pyglet.png
+nav_logo_text = pyglet
+
+footer_links = Github: https://github.com/pyglet/pyglet, Discord: https://discord.gg/QXyegWe, Google Groups: https://groups.google.com/d/forum/pyglet-users
+
diff --git a/website/themes/lektor-icon/AUTHORS.txt b/website/themes/lektor-icon/AUTHORS.txt
new file mode 100644
index 0000000..94aa3b9
--- /dev/null
+++ b/website/themes/lektor-icon/AUTHORS.txt
@@ -0,0 +1,14 @@
+The Lektor-Icon Contributors are composed of:
+
+* Free HTML5 Template <https://FreeHTML5.co> (Original HTML5 template author)
+* Steve Lane <https://gtown-ds.netlify.com/> (Port to Hugo theme)
+* Daniel Althviz <https://dalthviz.github.io/> (Port to Lektor theme)
+* All other developers that have contributed to the lektor-icon repository:
+  <https://github.com/spyder-ide/lektor-icon/graphs/contributors>
+
+Additionally, some font assets, Javascript libraries and stylesheets were
+originally sourced from third-party authors or projects.
+For information about the sources and authors of other third-party code
+and other resources used, please see the NOTICE.txt file, located in the
+root of the lektor-icon repository or online at:
+<https://github.com/spyder-ide/lektor-icon/blob/master/NOTICE.txt>
diff --git a/website/themes/lektor-icon/CHANGELOG.md b/website/themes/lektor-icon/CHANGELOG.md
new file mode 100644
index 0000000..1e846e4
--- /dev/null
+++ b/website/themes/lektor-icon/CHANGELOG.md
@@ -0,0 +1,61 @@
+# Lektor-Icon Changelog
+
+
+## 2019-02-01
+
+Feature additions:
+
+- Spyder blog module to theme and unify with existing templates
+- A generic page template/model with basic styling
+- A 404 error page template/model
+- A working, all new "mission" flowblock for the mainpage, and many fixes to others
+- Numerous new automatic/configurable features to individual flowblocks
+- A fully configurable footer with multiple sections
+- Optional integrations with Gitter, OpenCollective, Disqus comments, Atom/RSS feeds, video streaming sites and more
+- A configurable CSP and many other privacy and security improvements
+
+User-visible enhancements:
+
+- Refactor title, logo, and other elements hardcoded to Spyder-specific values to make theme actually usable on other sites
+- Rework single-page model/template to allow for a far more flexible and customizable layout with dynamic alternating background color
+- Make navigation bar display properly and automatically add section and page links
+- Make images responsive for faster loading and better quality
+- Add support for setting an overall theme/accent color in theme settings
+- Overhaul all layouts for much greater responsiveness across viewport sizes
+- Handle null various for essentially every setting so site degrades gracefully
+- Move most settings from theme to models for admin panel configuration and avoid breaking rebuilds
+- Add labels, descriptions, etc. to all model properties for easier configuration
+- Port over custom styles so site looks good out of the box
+- Refine design, add new styling cues and address accessibility issues
+- Fix dozens of layout bugs and issues
+
+Techdebt, refactoring and maintainability:
+
+- Unify existing models and template to remove large amount of duplication
+- Update theme, HTML, CSS, JS and HTTP headers to current standards
+- Expand Readme and add contributing guide for easier development
+- Add proper authors, licensing, third party notice information and other meta-files
+- Add images/, example site and other elements for inclusion in the Lektor themes repo and gallery
+- Refactor file, model/flowblock, class/id and property/option names for consistency and clarity
+- Factor out or remove no longer needed CSS, JS and fonts
+- Update vendored code (JQuery and friends) to modern, secure versions
+- Conform project to a suite of consistent best practices and conventions
+- Minify stylesheets and JS libraries
+
+
+## 2018-02-20
+
+- Port the theme to Lektor (@dalthviz)
+
+
+## 2017-11-14
+
+- Contact links were fixed [https://github.com/SteveLane/hugo-icon/issues/3](https://github.com/SteveLane/hugo-icon/issues/3) (@MartinWillitts)
+- Slight change to original main.js
+- A linkedin option was added to configuration (@MartinWillitts)
+
+
+## 2017-09-21
+
+- Theme ported to Hugo with no changes made to styling/theming
+- Adjusted contact form to work with netlify
diff --git a/website/themes/lektor-icon/CONTRIBUTING.md b/website/themes/lektor-icon/CONTRIBUTING.md
new file mode 100644
index 0000000..9527530
--- /dev/null
+++ b/website/themes/lektor-icon/CONTRIBUTING.md
@@ -0,0 +1,190 @@
+# Contributing to Lektor-Icon
+
+
+First off, thanks for your interest in helping out!
+
+**Important Note:** Keep in mind that the original and a continuing purpose of the code in this repository is to provide a high-quality, modern theme for the [Spyder](https://www.spyder-ide.org/) website.
+Therefore, all additions, changes and removals should should strive to retain compatibility, to the extent practicable, with its usage there.
+If not possible, please discuss them with us first.
+Thanks!
+
+For more guidance on the basics of using ``git`` and Github to contribute to Lektor-Icon and other projects, check out the tutorial in the [Spyder Development Documentation](https://github.com/spyder-ide/spyder/wiki/Contributing-to-Spyder) for detailed instructions, and be sure to see the [Spyder Contributing Guide](https://github.com/spyder-ide/spyder/blob/master/CONTRIBUTING.md) for [guidelines on adding third party content to Lektor-Icon](https://github.com/spyder-ide/spyder/blob/master/CONTRIBUTING.md#adding-third-party-content) (like images, fonts, CSS stylesheets and Javascript libraries, as well as Jinja2 templates from other projects).
+As always, feel free to contact us via one of the platforms listed at the bottom of this document if you have any questions or concerns, and we look forward to reviewing your contribution to Lektor-Icon!
+
+
+
+## Reporting Issues
+
+If you encounter an issue using Lektor-Icon, spot a bug in the code, or have a suggestion for improving it further, please let us know by submitting a report on our [Github Issue Tracker](https://github.com/spyder-ide/lektor-icon/issues).
+Make sure you provide as much information as possible to help us track down the issue, and we always appreciate offers to help resolve it yourself.
+Thanks!
+
+
+
+## Submitting Pull Requests
+
+We welcome contributions from the community, and will do our best to review all of them in a timely fashion.
+To do so, please fork this repository, create a new feature branch there based off the latest ``master``, make and test your changes, and then submit a pull request (PR) to this repo.
+Please make sure your PR titles are brief but descriptive, and include ``PR: `` as a prefix (if a work in progress, also prefix ``[WiP]``).
+
+You should also create a corresponding issue as well if your change is substantive, so that we can keep track of everything and give you credit for closing it.
+You might want to open an issue first discussing your changes, to get feedback and suggestions before implementing them.
+
+
+
+## Standards and Conventions
+
+Make sure you follow these to ensure clarity, consistency and correctness throughout our codebase.
+
+### All Files
+
+* **UTF-8** for character encoding
+* **LF** for newlines (our ``.gitattributes`` enforces this on commit)
+* **ISO 8601** (YYYY-MM-DD HH:MM:SS) for dates/times
+* **SI/metric** for units
+* **HTTPS** for all links where available (try adding it if the site is HTTP by default)
+* **Decimal point**, rather than decimal comma
+* **Forward slashes** (``/``) for path delimiters on all platforms (Windows accepts them equally to backslashes)
+* **Strip trailing spaces** on save
+* **Newline-terminate** all files
+* **Spaces, not tabs** except where e.g. JS files are unmodified or nearly so
+* **Lowercase filenames and extensions**, not all upper
+* **Dash-deliminate filenames** (``test-file.txt``), not underscore (``test_file.txt``)
+* **.txt** extension for all plain text files
+
+
+### All Lektor files (INI and ``contents.lr``)
+
+* **No hard wrap** after a fixed character value
+* **No indents/leading spaces** should be used
+* **Use ``true``/``false``, not ``yes``/``no``** for boolean values
+* **Include all keys**, even if values left blank
+* **Adhere to the Lektor theme spec** as published in the docs
+
+
+### Lektor ``contents.lr``
+
+* **Use a hierarchy of line breaks** between flowblock levels; respect existing convention
+* **Line breaks after ``---``** where needed, never before
+* **One blank line before multiline content**, i.e. blocks that span multiple lines
+* **Line break after sentences** (like this document)
+
+
+### INI Files (Models, Flowblocks, Lektorproject, etc)
+
+* **No quoting of values** should be employed
+* **One space around equals** on both sides
+* **One blank line between groups** of property values
+* **All lowercase names** for groups and keys unless required
+
+
+### Models and Flowblocks
+
+* **Name each model** clearly and appropriately
+* **Always include a label** for each item
+* **Include a short description** of each item for the admin UI; at least one sentence, but may extend to multiple if needed.
+* **Make titles size = large** unless in a multi-level nested flowblock
+* **Always include a checkbox label** for checkboxes
+* **Adhere to group and property order** as listed in the Lektor documentation
+
+
+### Jinja2 HTML Templates
+
+* **Ensure correct indent levels in output**; i.e. don't add indent levels solely for Jinja statements
+* **Indent jinja statements equally** to surrounding HTML
+* **One space after ``(%``, ``{{``, and ``|``/before ``%}``, ``}}`` and ``|``**
+* **Use ``'`` in Jinja, ``"`` in HTML** for quotes
+* **Follow spirit of PEP 8** for code style in Jinja expressions
+* **Use ``asseturl`` for anything in the ``assets/`` directory
+
+
+### HTML
+
+* **Validate with W3C HTML5 checker** against latest HTML standards
+* **Two space indent** for all files
+* **Don't use deprecated elements** in HTML5
+* **Avoid inline styles** if at all possible
+* **Don't close single tags** e.g. XHTML-style ``<br/>``
+* **Explicitly declare MIME types** to avoid content-sniffing
+* **Explicitly specify UTF-8 encoding** on elements where possible
+
+
+### CSS
+
+* **Validate with W3C CSS3 checker** against latest CSS standards
+* **No vendor prefixes** unless absolutely necessary; ensure parity between vendors
+* **Two space indent** for all CSS stylesheets
+* **One blank line between blocks** unless very closely linked
+* **One selector per line** unless extremely short
+* **Always terminate properties with ``;``** even if the last in the block
+* **Use six-digit hex for colors** unless transparency needed
+* **One space after ``:`` and before ``{``** except in pseudo-classes
+* **K&R style brackets** for each block
+* **Prefer ``em``/``rem`` to ``px``** where practicable
+
+
+### Javascript
+
+* **Follow existing code style** when it doubt
+* **Conform to modern ES6 best practices** where possible
+* **Four space indent** for new files
+* **Spaces after commas** except between linebreaks
+* **Spaces around binary operators** like PEP8
+* **K&R style brackets** for each block
+* **Use ``'`` for quotes**, except for HTML snippits
+* **Include descriptive comments**, at least one per function
+* **Maintain existing blank line hierarchy** between blocks
+* **Use minified version** of external libraries
+
+
+### Python
+
+* **Python 2/3 "universal" code** until the Python 2 EOL date
+* **PEP 8** style for Python code, with all recommendations treated as requirements unless noted
+* **PEP 257** for all docstrings
+* **79 characters** for line length
+* **Check code with ``pylint``** (or another linter) before submitting to catch potential errors and bad practices
+
+
+### Images
+
+* **SVG, PNG or JPEG for all images**; no exotic or proprietary formats
+* **SVG > PNG** when available for graphics and other vectorizable images
+* **PNG > JPEG, except for photos** whenever possible
+* **Size images appropriately** for the intended use; make use of responsive images when available
+* **Run ``optipng -o7``** on all PNGs; use moderate quality for JPEGs
+* **Alt text** should be always be provided describing the content of each image
+
+
+### Fonts
+
+* **Include TTF, WOFF and WOFF2** format files for each font as available
+* **Deploy only WOFF2 > WOFF** if present
+* **Subset fonts** if only a few characters are used
+
+
+### Documentation
+
+* **Github-Flavored Markdown** (GFM) for documentation like this one
+* **Universal Markdown/reStructured Text syntax** where practicable, e.g. double backtick instead of single for code, ``*`` for bullets instead of ``-``, and ``*``/``**`` for italic/bold instead of ``_``/``__``.
+* **No hard wrap** after a certain character value
+* **Line break after sentences** (like this document)
+* **3/2/1 lines before L2/3/4+ headings**, except for the first heading
+
+
+
+## Roadmap
+
+Nothing concrete at the moment, but a few ideas:
+
+* Refactor large ``style.css`` into separate stylesheets for blog/pages, mainpage and both
+* Refactor Javascript to use native ES6 instead of jQuery
+* Add polyfill/better fallback for theme coloring
+
+* Include per-services, team etc card to make the whole card a clickable link
+* Implement specific functionality around blog categories and tags
+* Make generic pages more sophisticated, and/or introduce additional page types
+
+
+
+Thanks so much!
diff --git a/website/themes/lektor-icon/LICENSE.txt b/website/themes/lektor-icon/LICENSE.txt
new file mode 100644
index 0000000..52cb329
--- /dev/null
+++ b/website/themes/lektor-icon/LICENSE.txt
@@ -0,0 +1,43 @@
+Original standalone HTML5 theme:
+
+Copyright (c) 2016 Free HTML5 Template <https://FreeHTML5.co>
+
+Released under the Creative Commons Attribution 3.0 license; see NOTICE.txt or
+<https://creativecommons.org/licenses/by/3.0/>
+
+-------------------------------------------------------------------------------
+
+Subsequent additions, porting and major modifications:
+
+Copyright (c) 2017 Steve Lane
+
+Copyright (c) 2018 Daniel Althviz
+
+Copyright (c) 2018-2019 Lektor-Icon Contributors (see AUTHORS.txt)
+
+Released under the MIT (Expat) license:
+
+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.
+
+-------------------------------------------------------------------------------
+
+Bundled assets:
+
+Released under MIT and other compatible licenses; see NOTICE.txt or online at:
+<https://github.com/spyder-ide/lektor-icon/blob/master/NOTICE.txt>
diff --git a/website/themes/lektor-icon/NOTICE.txt b/website/themes/lektor-icon/NOTICE.txt
new file mode 100644
index 0000000..2ae0894
--- /dev/null
+++ b/website/themes/lektor-icon/NOTICE.txt
@@ -0,0 +1,1213 @@
+Included Libraries, Fonts and Icons
+###################################
+
+
+General Information
+===================
+
+
+Lektor-Icon incorporates code and font assets from various external sources,
+each of which is covered by its own license and copyright notice.
+These files are listed here with their corresponding authors,
+permission notices, disclaimers, and copyright statements; these are also
+included in the files' directory and/or their headers, as appropriate.
+
+All files in this repository, including those not listed here, are covered by
+Lektor-Icon's MIT (Expat) license, described in the LICENSE.txt file.
+https://github.com/spyder-ide/lektor-icon/blob/master/LICENSE.txt
+
+Images, icons and font gyphs that include trademarked elements are used for
+informational purposes only, and do not necessarily constitute any endorsement,
+association or ownership by or of the marks or their respective owners.
+
+All trademarks are and remain the property of their respective owners.
+
+See https://github.com/spyder-ide/spyder/blob/master/NOTICE.txt
+for a guide and template on adding new assets to the project and this file.
+
+
+===============================================================================
+
+
+
+
+Predecessor Projects
+====================
+
+
+Icon: Free Website Template Using Bootstrap
+-------------------------------------------
+
+
+Copyright (c) 2016 Free HTML5 Template https://freehtml5.co/
+
+
+Author: Free HTML5 Template | https://freehtml5.co
+Site/Source: https://freehtml5.co/icon-free-website-template-using-bootstrap/
+License: Creative Commons Attribution 3.0 Unported
+https://creativecommons.org/licenses/by/3.0/
+
+Theme ported to Hugo, then to Lektor and extensive modifications made.
+
+
+Icon - 100% Fully Responsive Free HTML5 Bootstrap Template
+
+DESIGNED & DEVELOPED by FreeHTML5.co
+
+Website: https://freehtml5.co/
+Twitter: https://twitter.com/fh5co
+Facebook: https://facebook.com/fh5co
+
+All free templates that we’ve made are released under the
+Creative Commons Attribution 3.0 License which means you can:
+
+* Use them for personal stuff
+* Use them for commercial stuff
+* Change them however you like
+
+You can not:
+
+* Remove the attribution without purchasing a premium membership.
+
+Attribution — you MUST give APPROPRIATE CREDIT,
+PROVIDE A LINK TO FreeHTML5.co, and indicate if changes were made.
+
+However, you may remove back links or credit links to us by
+subscribing to our premium plans.
+https://freehtml5.co/membership-plan/
+
+
+See below for the full text of the CC-BY 3.0 license.
+
+The current FreeHTML5.co license can be viewed at:
+https://freehtml5.co/faq/#faq-license
+
+
+Files covered:
+
+assets/static/css/style.css
+assets/static/images/Preloader_2.gif
+assets/static/js/magnific-popup-options.js
+assets/static/js/main.js
+assets/static/js/main-singlelayout.js
+templates/layout.html
+templates/blocks/
+templates/single-layout.html
+templates/partials/hero.html
+
+
+-------------------------------------------------------------------------------
+
+
+
+Hugo Icon Theme
+---------------
+
+
+Copyright (c) 2017 Steve Lane
+
+
+Author: Steve Lane and contributors | https://gtown-ds.netlify.com/
+Site/Source: https://github.com/SteveLane/hugo-icon
+License: MIT (Expat) License | https://opensource.org/licenses/MIT
+
+Theme ported to Lektor and extensive modifications made.
+
+
+Credit for this theme goes fully to https://freehtml5.co/,
+which is licensed under a CC BY 3.0 license.
+If you use this Hugo port, please consider the terms of that license
+and make proper attribution to https://freehtml5.co/.
+
+
+See below for the full text of the MIT (Expat) license.
+
+The current Hugo Icon Theme license can be viewed at:
+https://github.com/SteveLane/hugo-icon/blob/master/LICENSE.md
+
+
+Files covered:
+
+assets/static/js/main.js
+templates/layout.html
+templates/single-layout.html
+templates/blocks/
+templates/partials/hero.html
+
+
+===============================================================================
+
+
+
+
+Third-Party Assets
+==================
+
+
+Bootstrap Stylesheet (with Glphyicons font) 3.3.5
+-------------------------------------------------
+
+
+Copyright (c) 2011-2015 Twitter, Inc.
+Copyright (c) 2011-2015 The Bootstrap Authors
+
+
+Author: Twitter, Inc and the Bootstrap core team
+Site: https://getbootstrap.com/
+Source: https://github.com/twbs/bootstrap
+License: MIT (Expat) License | https://opensource.org/licenses/MIT
+
+Minor style modifications made to boostrap.css stylesheet.
+
+
+Code copyright 2011-2015 the Bootstrap Authors and Twitter, Inc.
+Code released under the MIT License.
+Docs released under Creative Commons.
+
+
+See below for the full text of the MIT (Expat) license.
+
+The current Bootstrap license can be viewed at:
+https://github.com/twbs/bootstrap/blob/master/LICENSE
+
+
+Files covered:
+
+assets/static/css/boostrap.css
+assets/static/fonts/boostrap/
+
+
+-------------------------------------------------------------------------------
+
+
+
+Icomoon Icon Font
+-----------------
+
+
+Copyright (c) 2016 IcoMoon.io
+
+
+Author: Keyamoon | http://keyamoon.com
+Site: https://icomoon.io/
+Source: https://github.com/Keyamoon/IcoMoon-Free
+License: Creative Commons Attribution 4.0 International
+https://creativecommons.org/licenses/by/4.0/
+
+Minor font loading modifications made to icomoon.css stylesheet.
+
+
+IcoMoon-Free is a free vector icon pack by Keyamoon: http://keyamoon.com.
+
+You can use this package under one of these two licenses: CC BY 4.0 or GPL.
+
+https://creativecommons.org/licenses/by/4.0/
+https://www.gnu.org/licenses/gpl.html
+
+Visit IcoMoon.io: https://icomoon.io for more information.
+
+
+See below for the full text of the CC-BY 4.0 International license.
+
+The current Icomoon license can be viewed at:
+https://icomoon.io/#icons-icomoon
+
+
+Files covered:
+
+assets/static/css/icomoon.css
+assets/static/fonts/icomoon/
+
+
+-------------------------------------------------------------------------------
+
+
+
+Magnific Popup 1.1.0
+--------------------
+
+
+Copyright (c) 2014-2016 Dmitry Semenov, http://dimsemenov.com
+
+
+Author: Dmitry Semenov | diiiimaaaa@gmail.com
+Site: https://dimsemenov.com/plugins/magnific-popup/
+Source: https://github.com/dimsemenov/Magnific-Popup
+License: MIT (Expat) | https://opensource.org/licenses/MIT
+
+Minor modification to add font fallbacks to magnific-popup.css stylesheet.
+
+
+Script is MIT licensed and free and will always be kept this way.
+But has a small restriction from me - please do not create public WordPress
+plugin based on it(or at least contact me before creating it),
+because I will make it and it'll be open source too (want to get notified?).
+http://dimsemenov.com/subscribe.html
+
+Created by @dimsemenov & contributors.
+https://github.com/dimsemenov/Magnific-Popup/contributors
+
+
+See below for the full text of the MIT (Expat) license.
+
+The current Magnific license can be viewed at:
+https://github.com/dimsemenov/Magnific-Popup/blob/master/LICENSE
+
+
+Files covered:
+
+assets/static/css/magnific-popup.css
+assets/static/js/magnific-popup.js
+
+
+-------------------------------------------------------------------------------
+
+
+
+JQuery 3.3.1
+------------
+
+
+Copyright 2018 JS Foundation and other contributors, https://js.foundation/
+
+
+Author: JS Foundation and other contributors | https://js.foundation/
+Site: https://jquery.com/
+Source: https://github.com/jquery/jquery/
+License: MIT (Expat) | https://opensource.org/licenses/MIT
+
+No modifications made.
+
+
+Date: Wed Mar 21 12:46:34 2012 -0700
+
+This software consists of voluntary contributions made by many
+individuals. For exact contribution history, see the revision history
+available at https://github.com/jquery/jquery/
+
+The MIT License is simple and easy to understand and it places almost
+no restrictions on what you can do with the Project.
+
+You are free to use the Project in any other project (even commercial projects)
+as long as the copyright header is left intact.
+
+
+See below for the full text of the MIT (Expat) license.
+
+The current license summary can be viewed at:
+https://jquery.org/license
+
+The present license text can be referenced at:
+https://github.com/jquery/jquery/blob/master/LICENSE.txt
+
+
+Files covered:
+
+assets/static/js/jquery-3.3.1.min.js
+
+
+-------------------------------------------------------------------------------
+
+
+
+JQuery Easing 1.4.1 and Easing Equations
+----------------------------------------
+
+
+Copyright (c) 2001 Robert Penner
+Copyright (c) 2008- George McGinley Smith
+
+
+Author: George McGinley Smith and contributors | http://gsgd.co.uk/
+Site: http://gsgd.co.uk/sandbox/jquery/easing/
+Source: https://github.com/gdsmith/jquery.easing
+License: MIT (Expat) | https://opensource.org/licenses/MIT
+
+Minor modifications made to update script for jQuery 3.x.
+
+
+jQuery Easing v1.3.1 - http://gsgd.co.uk/sandbox/jquery/easing/
+
+Uses the built in easing capabilities added in jQuery 1.1
+to offer multiple easing options
+
+Open source under the BSD License.
+
+
+See below for the full text of the MIT (Expat) license.
+
+The current jQuery Easing license can be viewed at:
+https://github.com/gdsmith/jquery.easing/blob/master/LICENSE
+
+
+Files covered:
+
+assets/static/js/jquery.easing.min.js
+
+
+-------------------------------------------------------------------------------
+
+
+
+Stellar.js (jQuery) 0.6.2
+-------------------------
+
+
+Copyright (c) 2013 Mark Dalgleish
+
+
+Author: Mark Dalgleish and contributors | https://markdalgleish.com/
+Site: https://markdalgleish.com/projects/stellar.js/
+Source: https://github.com/markdalgleish/stellar.js
+License: MIT (Expat) | https://markdalgleish.mit-license.org
+
+Minor modifications made to update script for jQuery 3.x.
+
+
+Copyright 2013, Mark Dalgleish
+This content is released under the MIT license
+https://markdalgleish.mit-license.org
+
+
+See below for the full text of the MIT (Expat) license.
+
+The current Stellar.js license can be viewed at:
+https://github.com/markdalgleish/stellar.js/blob/master/LICENSE-MIT
+
+
+Files covered:
+
+assets/static/js/jquery.stellar.js
+assets/static/js/jquery.stellar.min.js
+
+
+-------------------------------------------------------------------------------
+
+
+
+Waypoints (jQuery) 4.0.1
+------------------------
+
+
+Copyright (c) 2011-2014 Caleb Troughton
+
+
+Author: Caleb Troughton and contributors | http://imakewebthings.com/
+Site: http://imakewebthings.com/waypoints/
+Source: https://github.com/imakewebthings/waypoints
+License: MIT (Expat) | https://opensource.org/licenses/MIT
+
+Minor modifications made to update script for jQuery 3.x.
+
+
+Copyright (c) 2011-2014 Caleb Troughton. Licensed under the MIT license.
+https://github.com/imakewebthings/waypoints/blob/master/licenses.txt
+
+
+See below for the full text of the MIT (Expat) license.
+
+The current Waypoints license can be viewed at:
+https://github.com/imakewebthings/waypoints/blob/master/licenses.txt
+
+
+Files covered:
+
+assets/static/js/jquery.waypoints.min.js
+
+
+===============================================================================
+
+
+
+
+Full License Text
+=================
+
+
+MIT (Expat) License
+-------------------
+
+
+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.
+
+
+
+-------------------------------------------------------------------------------
+
+
+
+3-Clause ("Modified") BSD License
+---------------------------------
+
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of the <organization> nor the
+      names of its contributors may be used to endorse or promote products
+      derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+-------------------------------------------------------------------------------
+
+
+
+Creative Commons Attribution 3.0 Unported License
+-------------------------------------------------
+
+
+Creative Commons Legal Code
+
+Attribution 3.0 Unported
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR
+    DAMAGES RESULTING FROM ITS USE.
+
+License
+
+THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
+COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
+COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
+AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
+
+BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
+TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY
+BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS
+CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND
+CONDITIONS.
+
+1. Definitions
+
+ a. "Adaptation" means a work based upon the Work, or upon the Work and
+    other pre-existing works, such as a translation, adaptation,
+    derivative work, arrangement of music or other alterations of a
+    literary or artistic work, or phonogram or performance and includes
+    cinematographic adaptations or any other form in which the Work may be
+    recast, transformed, or adapted including in any form recognizably
+    derived from the original, except that a work that constitutes a
+    Collection will not be considered an Adaptation for the purpose of
+    this License. For the avoidance of doubt, where the Work is a musical
+    work, performance or phonogram, the synchronization of the Work in
+    timed-relation with a moving image ("synching") will be considered an
+    Adaptation for the purpose of this License.
+ b. "Collection" means a collection of literary or artistic works, such as
+    encyclopedias and anthologies, or performances, phonograms or
+    broadcasts, or other works or subject matter other than works listed
+    in Section 1(f) below, which, by reason of the selection and
+    arrangement of their contents, constitute intellectual creations, in
+    which the Work is included in its entirety in unmodified form along
+    with one or more other contributions, each constituting separate and
+    independent works in themselves, which together are assembled into a
+    collective whole. A work that constitutes a Collection will not be
+    considered an Adaptation (as defined above) for the purposes of this
+    License.
+ c. "Distribute" means to make available to the public the original and
+    copies of the Work or Adaptation, as appropriate, through sale or
+    other transfer of ownership.
+ d. "Licensor" means the individual, individuals, entity or entities that
+    offer(s) the Work under the terms of this License.
+ e. "Original Author" means, in the case of a literary or artistic work,
+    the individual, individuals, entity or entities who created the Work
+    or if no individual or entity can be identified, the publisher; and in
+    addition (i) in the case of a performance the actors, singers,
+    musicians, dancers, and other persons who act, sing, deliver, declaim,
+    play in, interpret or otherwise perform literary or artistic works or
+    expressions of folklore; (ii) in the case of a phonogram the producer
+    being the person or legal entity who first fixes the sounds of a
+    performance or other sounds; and, (iii) in the case of broadcasts, the
+    organization that transmits the broadcast.
+ f. "Work" means the literary and/or artistic work offered under the terms
+    of this License including without limitation any production in the
+    literary, scientific and artistic domain, whatever may be the mode or
+    form of its expression including digital form, such as a book,
+    pamphlet and other writing; a lecture, address, sermon or other work
+    of the same nature; a dramatic or dramatico-musical work; a
+    choreographic work or entertainment in dumb show; a musical
+    composition with or without words; a cinematographic work to which are
+    assimilated works expressed by a process analogous to cinematography;
+    a work of drawing, painting, architecture, sculpture, engraving or
+    lithography; a photographic work to which are assimilated works
+    expressed by a process analogous to photography; a work of applied
+    art; an illustration, map, plan, sketch or three-dimensional work
+    relative to geography, topography, architecture or science; a
+    performance; a broadcast; a phonogram; a compilation of data to the
+    extent it is protected as a copyrightable work; or a work performed by
+    a variety or circus performer to the extent it is not otherwise
+    considered a literary or artistic work.
+ g. "You" means an individual or entity exercising rights under this
+    License who has not previously violated the terms of this License with
+    respect to the Work, or who has received express permission from the
+    Licensor to exercise rights under this License despite a previous
+    violation.
+ h. "Publicly Perform" means to perform public recitations of the Work and
+    to communicate to the public those public recitations, by any means or
+    process, including by wire or wireless means or public digital
+    performances; to make available to the public Works in such a way that
+    members of the public may access these Works from a place and at a
+    place individually chosen by them; to perform the Work to the public
+    by any means or process and the communication to the public of the
+    performances of the Work, including by public digital performance; to
+    broadcast and rebroadcast the Work by any means including signs,
+    sounds or images.
+ i. "Reproduce" means to make copies of the Work by any means including
+    without limitation by sound or visual recordings and the right of
+    fixation and reproducing fixations of the Work, including storage of a
+    protected performance or phonogram in digital form or other electronic
+    medium.
+
+2. Fair Dealing Rights. Nothing in this License is intended to reduce,
+limit, or restrict any uses free from copyright or rights arising from
+limitations or exceptions that are provided for in connection with the
+copyright protection under copyright law or other applicable laws.
+
+3. License Grant. Subject to the terms and conditions of this License,
+Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
+perpetual (for the duration of the applicable copyright) license to
+exercise the rights in the Work as stated below:
+
+ a. to Reproduce the Work, to incorporate the Work into one or more
+    Collections, and to Reproduce the Work as incorporated in the
+    Collections;
+ b. to create and Reproduce Adaptations provided that any such Adaptation,
+    including any translation in any medium, takes reasonable steps to
+    clearly label, demarcate or otherwise identify that changes were made
+    to the original Work. For example, a translation could be marked "The
+    original work was translated from English to Spanish," or a
+    modification could indicate "The original work has been modified.";
+ c. to Distribute and Publicly Perform the Work including as incorporated
+    in Collections; and,
+ d. to Distribute and Publicly Perform Adaptations.
+ e. For the avoidance of doubt:
+
+     i. Non-waivable Compulsory License Schemes. In those jurisdictions in
+        which the right to collect royalties through any statutory or
+        compulsory licensing scheme cannot be waived, the Licensor
+        reserves the exclusive right to collect such royalties for any
+        exercise by You of the rights granted under this License;
+    ii. Waivable Compulsory License Schemes. In those jurisdictions in
+        which the right to collect royalties through any statutory or
+        compulsory licensing scheme can be waived, the Licensor waives the
+        exclusive right to collect such royalties for any exercise by You
+        of the rights granted under this License; and,
+   iii. Voluntary License Schemes. The Licensor waives the right to
+        collect royalties, whether individually or, in the event that the
+        Licensor is a member of a collecting society that administers
+        voluntary licensing schemes, via that society, from any exercise
+        by You of the rights granted under this License.
+
+The above rights may be exercised in all media and formats whether now
+known or hereafter devised. The above rights include the right to make
+such modifications as are technically necessary to exercise the rights in
+other media and formats. Subject to Section 8(f), all rights not expressly
+granted by Licensor are hereby reserved.
+
+4. Restrictions. The license granted in Section 3 above is expressly made
+subject to and limited by the following restrictions:
+
+ a. You may Distribute or Publicly Perform the Work only under the terms
+    of this License. You must include a copy of, or the Uniform Resource
+    Identifier (URI) for, this License with every copy of the Work You
+    Distribute or Publicly Perform. You may not offer or impose any terms
+    on the Work that restrict the terms of this License or the ability of
+    the recipient of the Work to exercise the rights granted to that
+    recipient under the terms of the License. You may not sublicense the
+    Work. You must keep intact all notices that refer to this License and
+    to the disclaimer of warranties with every copy of the Work You
+    Distribute or Publicly Perform. When You Distribute or Publicly
+    Perform the Work, You may not impose any effective technological
+    measures on the Work that restrict the ability of a recipient of the
+    Work from You to exercise the rights granted to that recipient under
+    the terms of the License. This Section 4(a) applies to the Work as
+    incorporated in a Collection, but this does not require the Collection
+    apart from the Work itself to be made subject to the terms of this
+    License. If You create a Collection, upon notice from any Licensor You
+    must, to the extent practicable, remove from the Collection any credit
+    as required by Section 4(b), as requested. If You create an
+    Adaptation, upon notice from any Licensor You must, to the extent
+    practicable, remove from the Adaptation any credit as required by
+    Section 4(b), as requested.
+ b. If You Distribute, or Publicly Perform the Work or any Adaptations or
+    Collections, You must, unless a request has been made pursuant to
+    Section 4(a), keep intact all copyright notices for the Work and
+    provide, reasonable to the medium or means You are utilizing: (i) the
+    name of the Original Author (or pseudonym, if applicable) if supplied,
+    and/or if the Original Author and/or Licensor designate another party
+    or parties (e.g., a sponsor institute, publishing entity, journal) for
+    attribution ("Attribution Parties") in Licensor's copyright notice,
+    terms of service or by other reasonable means, the name of such party
+    or parties; (ii) the title of the Work if supplied; (iii) to the
+    extent reasonably practicable, the URI, if any, that Licensor
+    specifies to be associated with the Work, unless such URI does not
+    refer to the copyright notice or licensing information for the Work;
+    and (iv) , consistent with Section 3(b), in the case of an Adaptation,
+    a credit identifying the use of the Work in the Adaptation (e.g.,
+    "French translation of the Work by Original Author," or "Screenplay
+    based on original Work by Original Author"). The credit required by
+    this Section 4 (b) may be implemented in any reasonable manner;
+    provided, however, that in the case of a Adaptation or Collection, at
+    a minimum such credit will appear, if a credit for all contributing
+    authors of the Adaptation or Collection appears, then as part of these
+    credits and in a manner at least as prominent as the credits for the
+    other contributing authors. For the avoidance of doubt, You may only
+    use the credit required by this Section for the purpose of attribution
+    in the manner set out above and, by exercising Your rights under this
+    License, You may not implicitly or explicitly assert or imply any
+    connection with, sponsorship or endorsement by the Original Author,
+    Licensor and/or Attribution Parties, as appropriate, of You or Your
+    use of the Work, without the separate, express prior written
+    permission of the Original Author, Licensor and/or Attribution
+    Parties.
+ c. Except as otherwise agreed in writing by the Licensor or as may be
+    otherwise permitted by applicable law, if You Reproduce, Distribute or
+    Publicly Perform the Work either by itself or as part of any
+    Adaptations or Collections, You must not distort, mutilate, modify or
+    take other derogatory action in relation to the Work which would be
+    prejudicial to the Original Author's honor or reputation. Licensor
+    agrees that in those jurisdictions (e.g. Japan), in which any exercise
+    of the right granted in Section 3(b) of this License (the right to
+    make Adaptations) would be deemed to be a distortion, mutilation,
+    modification or other derogatory action prejudicial to the Original
+    Author's honor and reputation, the Licensor will waive or not assert,
+    as appropriate, this Section, to the fullest extent permitted by the
+    applicable national law, to enable You to reasonably exercise Your
+    right under Section 3(b) of this License (right to make Adaptations)
+    but not otherwise.
+
+5. Representations, Warranties and Disclaimer
+
+UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
+OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
+KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
+INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
+FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
+LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
+WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION
+OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
+
+6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE
+LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR
+ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES
+ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS
+BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+7. Termination
+
+ a. This License and the rights granted hereunder will terminate
+    automatically upon any breach by You of the terms of this License.
+    Individuals or entities who have received Adaptations or Collections
+    from You under this License, however, will not have their licenses
+    terminated provided such individuals or entities remain in full
+    compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will
+    survive any termination of this License.
+ b. Subject to the above terms and conditions, the license granted here is
+    perpetual (for the duration of the applicable copyright in the Work).
+    Notwithstanding the above, Licensor reserves the right to release the
+    Work under different license terms or to stop distributing the Work at
+    any time; provided, however that any such election will not serve to
+    withdraw this License (or any other license that has been, or is
+    required to be, granted under the terms of this License), and this
+    License will continue in full force and effect unless terminated as
+    stated above.
+
+8. Miscellaneous
+
+ a. Each time You Distribute or Publicly Perform the Work or a Collection,
+    the Licensor offers to the recipient a license to the Work on the same
+    terms and conditions as the license granted to You under this License.
+ b. Each time You Distribute or Publicly Perform an Adaptation, Licensor
+    offers to the recipient a license to the original Work on the same
+    terms and conditions as the license granted to You under this License.
+ c. If any provision of this License is invalid or unenforceable under
+    applicable law, it shall not affect the validity or enforceability of
+    the remainder of the terms of this License, and without further action
+    by the parties to this agreement, such provision shall be reformed to
+    the minimum extent necessary to make such provision valid and
+    enforceable.
+ d. No term or provision of this License shall be deemed waived and no
+    breach consented to unless such waiver or consent shall be in writing
+    and signed by the party to be charged with such waiver or consent.
+ e. This License constitutes the entire agreement between the parties with
+    respect to the Work licensed here. There are no understandings,
+    agreements or representations with respect to the Work not specified
+    here. Licensor shall not be bound by any additional provisions that
+    may appear in any communication from You. This License may not be
+    modified without the mutual written agreement of the Licensor and You.
+ f. The rights granted under, and the subject matter referenced, in this
+    License were drafted utilizing the terminology of the Berne Convention
+    for the Protection of Literary and Artistic Works (as amended on
+    September 28, 1979), the Rome Convention of 1961, the WIPO Copyright
+    Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996
+    and the Universal Copyright Convention (as revised on July 24, 1971).
+    These rights and subject matter take effect in the relevant
+    jurisdiction in which the License terms are sought to be enforced
+    according to the corresponding provisions of the implementation of
+    those treaty provisions in the applicable national law. If the
+    standard suite of rights granted under applicable copyright law
+    includes additional rights not granted under this License, such
+    additional rights are deemed to be included in the License; this
+    License is not intended to restrict the license of any rights under
+    applicable law.
+
+
+Creative Commons Notice
+
+    Creative Commons is not a party to this License, and makes no warranty
+    whatsoever in connection with the Work. Creative Commons will not be
+    liable to You or any party on any legal theory for any damages
+    whatsoever, including without limitation any general, special,
+    incidental or consequential damages arising in connection to this
+    license. Notwithstanding the foregoing two (2) sentences, if Creative
+    Commons has expressly identified itself as the Licensor hereunder, it
+    shall have all rights and obligations of Licensor.
+
+    Except for the limited purpose of indicating to the public that the
+    Work is licensed under the CCPL, Creative Commons does not authorize
+    the use by either party of the trademark "Creative Commons" or any
+    related trademark or logo of Creative Commons without the prior
+    written consent of Creative Commons. Any permitted use will be in
+    compliance with Creative Commons' then-current trademark usage
+    guidelines, as may be published on its website or otherwise made
+    available upon request from time to time. For the avoidance of doubt,
+    this trademark restriction does not form part of this License.
+
+    Creative Commons may be contacted at https://creativecommons.org/.
+
+
+-------------------------------------------------------------------------------
+
+
+
+Creative Commons Attribution 4.0 International License
+------------------------------------------------------
+
+
+Attribution 4.0 International
+
+=======================================================================
+
+Creative Commons Corporation ("Creative Commons") is not a law firm and
+does not provide legal services or legal advice. Distribution of
+Creative Commons public licenses does not create a lawyer-client or
+other relationship. Creative Commons makes its licenses and related
+information available on an "as-is" basis. Creative Commons gives no
+warranties regarding its licenses, any material licensed under their
+terms and conditions, or any related information. Creative Commons
+disclaims all liability for damages resulting from their use to the
+fullest extent possible.
+
+Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and
+conditions that creators and other rights holders may use to share
+original works of authorship and other material subject to copyright
+and certain other rights specified in the public license below. The
+following considerations are for informational purposes only, are not
+exhaustive, and do not form part of our licenses.
+
+     Considerations for licensors: Our public licenses are
+     intended for use by those authorized to give the public
+     permission to use material in ways otherwise restricted by
+     copyright and certain other rights. Our licenses are
+     irrevocable. Licensors should read and understand the terms
+     and conditions of the license they choose before applying it.
+     Licensors should also secure all rights necessary before
+     applying our licenses so that the public can reuse the
+     material as expected. Licensors should clearly mark any
+     material not subject to the license. This includes other CC-
+     licensed material, or material used under an exception or
+     limitation to copyright. More considerations for licensors:
+     wiki.creativecommons.org/Considerations_for_licensors
+
+     Considerations for the public: By using one of our public
+     licenses, a licensor grants the public permission to use the
+     licensed material under specified terms and conditions. If
+     the licensor's permission is not necessary for any reason--for
+     example, because of any applicable exception or limitation to
+     copyright--then that use is not regulated by the license. Our
+     licenses grant only permissions under copyright and certain
+     other rights that a licensor has authority to grant. Use of
+     the licensed material may still be restricted for other
+     reasons, including because others have copyright or other
+     rights in the material. A licensor may make special requests,
+     such as asking that all changes be marked or described.
+     Although not required by our licenses, you are encouraged to
+     respect those requests where reasonable. More considerations
+     for the public:
+     wiki.creativecommons.org/Considerations_for_licensees
+
+=======================================================================
+
+Creative Commons Attribution 4.0 International Public License
+
+By exercising the Licensed Rights (defined below), You accept and agree
+to be bound by the terms and conditions of this Creative Commons
+Attribution 4.0 International Public License ("Public License"). To the
+extent this Public License may be interpreted as a contract, You are
+granted the Licensed Rights in consideration of Your acceptance of
+these terms and conditions, and the Licensor grants You such rights in
+consideration of benefits the Licensor receives from making the
+Licensed Material available under these terms and conditions.
+
+
+Section 1 -- Definitions.
+
+  a. Adapted Material means material subject to Copyright and Similar
+     Rights that is derived from or based upon the Licensed Material
+     and in which the Licensed Material is translated, altered,
+     arranged, transformed, or otherwise modified in a manner requiring
+     permission under the Copyright and Similar Rights held by the
+     Licensor. For purposes of this Public License, where the Licensed
+     Material is a musical work, performance, or sound recording,
+     Adapted Material is always produced where the Licensed Material is
+     synched in timed relation with a moving image.
+
+  b. Adapter's License means the license You apply to Your Copyright
+     and Similar Rights in Your contributions to Adapted Material in
+     accordance with the terms and conditions of this Public License.
+
+  c. Copyright and Similar Rights means copyright and/or similar rights
+     closely related to copyright including, without limitation,
+     performance, broadcast, sound recording, and Sui Generis Database
+     Rights, without regard to how the rights are labeled or
+     categorized. For purposes of this Public License, the rights
+     specified in Section 2(b)(1)-(2) are not Copyright and Similar
+     Rights.
+
+  d. Effective Technological Measures means those measures that, in the
+     absence of proper authority, may not be circumvented under laws
+     fulfilling obligations under Article 11 of the WIPO Copyright
+     Treaty adopted on December 20, 1996, and/or similar international
+     agreements.
+
+  e. Exceptions and Limitations means fair use, fair dealing, and/or
+     any other exception or limitation to Copyright and Similar Rights
+     that applies to Your use of the Licensed Material.
+
+  f. Licensed Material means the artistic or literary work, database,
+     or other material to which the Licensor applied this Public
+     License.
+
+  g. Licensed Rights means the rights granted to You subject to the
+     terms and conditions of this Public License, which are limited to
+     all Copyright and Similar Rights that apply to Your use of the
+     Licensed Material and that the Licensor has authority to license.
+
+  h. Licensor means the individual(s) or entity(ies) granting rights
+     under this Public License.
+
+  i. Share means to provide material to the public by any means or
+     process that requires permission under the Licensed Rights, such
+     as reproduction, public display, public performance, distribution,
+     dissemination, communication, or importation, and to make material
+     available to the public including in ways that members of the
+     public may access the material from a place and at a time
+     individually chosen by them.
+
+  j. Sui Generis Database Rights means rights other than copyright
+     resulting from Directive 96/9/EC of the European Parliament and of
+     the Council of 11 March 1996 on the legal protection of databases,
+     as amended and/or succeeded, as well as other essentially
+     equivalent rights anywhere in the world.
+
+  k. You means the individual or entity exercising the Licensed Rights
+     under this Public License. Your has a corresponding meaning.
+
+
+Section 2 -- Scope.
+
+  a. License grant.
+
+       1. Subject to the terms and conditions of this Public License,
+          the Licensor hereby grants You a worldwide, royalty-free,
+          non-sublicensable, non-exclusive, irrevocable license to
+          exercise the Licensed Rights in the Licensed Material to:
+
+            a. reproduce and Share the Licensed Material, in whole or
+               in part; and
+
+            b. produce, reproduce, and Share Adapted Material.
+
+       2. Exceptions and Limitations. For the avoidance of doubt, where
+          Exceptions and Limitations apply to Your use, this Public
+          License does not apply, and You do not need to comply with
+          its terms and conditions.
+
+       3. Term. The term of this Public License is specified in Section
+          6(a).
+
+       4. Media and formats; technical modifications allowed. The
+          Licensor authorizes You to exercise the Licensed Rights in
+          all media and formats whether now known or hereafter created,
+          and to make technical modifications necessary to do so. The
+          Licensor waives and/or agrees not to assert any right or
+          authority to forbid You from making technical modifications
+          necessary to exercise the Licensed Rights, including
+          technical modifications necessary to circumvent Effective
+          Technological Measures. For purposes of this Public License,
+          simply making modifications authorized by this Section 2(a)
+          (4) never produces Adapted Material.
+
+       5. Downstream recipients.
+
+            a. Offer from the Licensor -- Licensed Material. Every
+               recipient of the Licensed Material automatically
+               receives an offer from the Licensor to exercise the
+               Licensed Rights under the terms and conditions of this
+               Public License.
+
+            b. No downstream restrictions. You may not offer or impose
+               any additional or different terms or conditions on, or
+               apply any Effective Technological Measures to, the
+               Licensed Material if doing so restricts exercise of the
+               Licensed Rights by any recipient of the Licensed
+               Material.
+
+       6. No endorsement. Nothing in this Public License constitutes or
+          may be construed as permission to assert or imply that You
+          are, or that Your use of the Licensed Material is, connected
+          with, or sponsored, endorsed, or granted official status by,
+          the Licensor or others designated to receive attribution as
+          provided in Section 3(a)(1)(A)(i).
+
+  b. Other rights.
+
+       1. Moral rights, such as the right of integrity, are not
+          licensed under this Public License, nor are publicity,
+          privacy, and/or other similar personality rights; however, to
+          the extent possible, the Licensor waives and/or agrees not to
+          assert any such rights held by the Licensor to the limited
+          extent necessary to allow You to exercise the Licensed
+          Rights, but not otherwise.
+
+       2. Patent and trademark rights are not licensed under this
+          Public License.
+
+       3. To the extent possible, the Licensor waives any right to
+          collect royalties from You for the exercise of the Licensed
+          Rights, whether directly or through a collecting society
+          under any voluntary or waivable statutory or compulsory
+          licensing scheme. In all other cases the Licensor expressly
+          reserves any right to collect such royalties.
+
+
+Section 3 -- License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the
+following conditions.
+
+  a. Attribution.
+
+       1. If You Share the Licensed Material (including in modified
+          form), You must:
+
+            a. retain the following if it is supplied by the Licensor
+               with the Licensed Material:
+
+                 i. identification of the creator(s) of the Licensed
+                    Material and any others designated to receive
+                    attribution, in any reasonable manner requested by
+                    the Licensor (including by pseudonym if
+                    designated);
+
+                ii. a copyright notice;
+
+               iii. a notice that refers to this Public License;
+
+                iv. a notice that refers to the disclaimer of
+                    warranties;
+
+                 v. a URI or hyperlink to the Licensed Material to the
+                    extent reasonably practicable;
+
+            b. indicate if You modified the Licensed Material and
+               retain an indication of any previous modifications; and
+
+            c. indicate the Licensed Material is licensed under this
+               Public License, and include the text of, or the URI or
+               hyperlink to, this Public License.
+
+       2. You may satisfy the conditions in Section 3(a)(1) in any
+          reasonable manner based on the medium, means, and context in
+          which You Share the Licensed Material. For example, it may be
+          reasonable to satisfy the conditions by providing a URI or
+          hyperlink to a resource that includes the required
+          information.
+
+       3. If requested by the Licensor, You must remove any of the
+          information required by Section 3(a)(1)(A) to the extent
+          reasonably practicable.
+
+       4. If You Share Adapted Material You produce, the Adapter's
+          License You apply must not prevent recipients of the Adapted
+          Material from complying with this Public License.
+
+
+Section 4 -- Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that
+apply to Your use of the Licensed Material:
+
+  a. for the avoidance of doubt, Section 2(a)(1) grants You the right
+     to extract, reuse, reproduce, and Share all or a substantial
+     portion of the contents of the database;
+
+  b. if You include all or a substantial portion of the database
+     contents in a database in which You have Sui Generis Database
+     Rights, then the database in which You have Sui Generis Database
+     Rights (but not its individual contents) is Adapted Material; and
+
+  c. You must comply with the conditions in Section 3(a) if You Share
+     all or a substantial portion of the contents of the database.
+
+For the avoidance of doubt, this Section 4 supplements and does not
+replace Your obligations under this Public License where the Licensed
+Rights include other Copyright and Similar Rights.
+
+
+Section 5 -- Disclaimer of Warranties and Limitation of Liability.
+
+  a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
+     EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
+     AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
+     ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
+     IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
+     WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
+     PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
+     ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
+     KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
+     ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
+
+  b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
+     TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
+     NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
+     INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
+     COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
+     USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
+     ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
+     DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
+     IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
+
+  c. The disclaimer of warranties and limitation of liability provided
+     above shall be interpreted in a manner that, to the extent
+     possible, most closely approximates an absolute disclaimer and
+     waiver of all liability.
+
+
+Section 6 -- Term and Termination.
+
+  a. This Public License applies for the term of the Copyright and
+     Similar Rights licensed here. However, if You fail to comply with
+     this Public License, then Your rights under this Public License
+     terminate automatically.
+
+  b. Where Your right to use the Licensed Material has terminated under
+     Section 6(a), it reinstates:
+
+       1. automatically as of the date the violation is cured, provided
+          it is cured within 30 days of Your discovery of the
+          violation; or
+
+       2. upon express reinstatement by the Licensor.
+
+     For the avoidance of doubt, this Section 6(b) does not affect any
+     right the Licensor may have to seek remedies for Your violations
+     of this Public License.
+
+  c. For the avoidance of doubt, the Licensor may also offer the
+     Licensed Material under separate terms or conditions or stop
+     distributing the Licensed Material at any time; however, doing so
+     will not terminate this Public License.
+
+  d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
+     License.
+
+
+Section 7 -- Other Terms and Conditions.
+
+  a. The Licensor shall not be bound by any additional or different
+     terms or conditions communicated by You unless expressly agreed.
+
+  b. Any arrangements, understandings, or agreements regarding the
+     Licensed Material not stated herein are separate from and
+     independent of the terms and conditions of this Public License.
+
+
+Section 8 -- Interpretation.
+
+  a. For the avoidance of doubt, this Public License does not, and
+     shall not be interpreted to, reduce, limit, restrict, or impose
+     conditions on any use of the Licensed Material that could lawfully
+     be made without permission under this Public License.
+
+  b. To the extent possible, if any provision of this Public License is
+     deemed unenforceable, it shall be automatically reformed to the
+     minimum extent necessary to make it enforceable. If the provision
+     cannot be reformed, it shall be severed from this Public License
+     without affecting the enforceability of the remaining terms and
+     conditions.
+
+  c. No term or condition of this Public License will be waived and no
+     failure to comply consented to unless expressly agreed to by the
+     Licensor.
+
+  d. Nothing in this Public License constitutes or may be interpreted
+     as a limitation upon, or waiver of, any privileges and immunities
+     that apply to the Licensor or You, including from the legal
+     processes of any jurisdiction or authority.
+
+
+=======================================================================
+
+Creative Commons is not a party to its public
+licenses. Notwithstanding, Creative Commons may elect to apply one of
+its public licenses to material it publishes and in those instances
+will be considered the “Licensor.” The text of the Creative Commons
+public licenses is dedicated to the public domain under the CC0 Public
+Domain Dedication. Except for the limited purpose of indicating that
+material is shared under a Creative Commons public license or as
+otherwise permitted by the Creative Commons policies published at
+creativecommons.org/policies, Creative Commons does not authorize the
+use of the trademark "Creative Commons" or any other trademark or logo
+of Creative Commons without its prior written consent including,
+without limitation, in connection with any unauthorized modifications
+to any of its public licenses or any other arrangements,
+understandings, or agreements concerning use of licensed material. For
+the avoidance of doubt, this paragraph does not form part of the
+public licenses.
+
+Creative Commons may be contacted at creativecommons.org.
diff --git a/website/themes/lektor-icon/README.md b/website/themes/lektor-icon/README.md
new file mode 100755
index 0000000..14e779b
--- /dev/null
+++ b/website/themes/lektor-icon/README.md
@@ -0,0 +1,386 @@
+# Lektor-Icon Theme
+
+*Copyright © 2016-2019 Lektor-Icon Contributors and others (see [AUTHORS.txt](https://github.com/spyder-ide/lektor-icon/blob/master/AUTHORS.txt))*
+
+![Screenshot of Lektor-Icon screenshot section](./images/mainpage-screenshots.png)
+
+This a heavily modified and expanded version of the [FreeHTML5](https://freehtml5.co/) ["Icon" template](https://freehtml5.co/icon-free-website-template-using-bootstrap/) ported to the [Lektor](https://www.getlektor.com/) static CMS by [Daniel Althviz](https://dalthviz.github.io/) and the [Spyder](https://www.spyder-ide.org/) development team; it was initially derived from the [Hugo port](https://github.com/SteveLane/hugo-icon) of the Icon theme by [Steve Lane](https://gtown-ds.netlify.com/).
+The core template is a single-page, responsive layout, with sections for describing your organization and its mission, services, a gallery, your team and how visitors can download your software or donate to your cause.
+It also features additions including a built-in blog, generic page template, custom error page, common navbar, Gitter, Disqus, RSS/Atom and OpenCollective integration, heavy customizability, numerous fixes and improvements, and re-written for speed, extensibility, responsiveness and conformance.
+The theme is designed to be more desktop and mobile-friendly, and designed and tested to smoothly adapt to a wide variety of viewport sizes from well below 480px all the way to 4K, and closely follows modern web standards while still offering compatibility with a wide range of modern and legacy browsers.
+
+
+
+## Installation
+
+See the [Lektor theme docs](https://www.getlektor.com/docs/themes/).
+We recommend installing Lektor-Icon as a Git submodule, as we often update and improve it from time to time.
+The theme should support Lektor versions 3.1.0 and later like any other, but we recommend Lektor 3.1.2 for the best results since it has been most extensively tested with that version, and we plan to update it with the responsive image improvements in the next version of Lektor when those become available.
+
+No plugins are required with the default settings, but enabling support for Disqus comments and a RSS/Atom feed for the blog naturally demands the inclusion of those appropriate package names in the ``[packages]`` section of your ``.lektorproject`` file.
+The theme doesn't depend on any ``.lektorproject`` settings aside from its own ``[theme_settings]`` configuration to function properly, and rather uses its own where needed, so you are free to keep its configuration variables fully separate from any you may make use of.
+
+
+
+## Basic Setup
+
+### Quickstart
+
+To get a baseline site up and running fast and start playing around with what the theme can do, your quickest option is to just copy the example site's ``content`` directory to your own and edit away.
+By default, the sample site has a variety of information about the theme and how to use it to help you make the most of its features and capabilities.
+The main "Single Page Site" template is ``singlepage``, more conventional pages can be created with the ``page`` template, a blog and posts to it can be added with the respective ``blog`` and ``blog-post`` templates, centralized author management is implemented with the ``authors`` and ``author`` templates, and there's also a ``404`` template for a custom error page.
+That said, if you're already familiar with how to organize your site, want to better understand how the pieces fit together by doing it from scratch or is just the type who likes to "do it yourself", the following step-by-step procedure should guide you through the process.
+
+
+### Step by Step Manual Setup
+
+All steps aside from the first are entirely optional, depending on what pages and content you'd like to include, and the site should build successfully without anything else starting from that step.
+Except for the initial creation of one-off pages (homepage, blog index, authors index and 404.html), all steps can performed from the Lektor admin GUI, if you so choose (aside from the obvious exclusions of configuring global theme settings and plugins, and copying in image assets and editing custom CSS stylesheets that Lektor doesn't (yet) support from the web GUI).
+
+0. Initialize a new empty Lektor project with a ``.lektorproject`` file and install the theme (see previous section).
+
+1. In the project's ``content`` directory, create a ``contents.lr`` for your homepage, with ``_model: singlepage``.
+   You could use a plain ``page`` for your ``_model`` if you really wanted, but most users will want to use the ``singlepage`` model for the full experience.
+
+2. If you'd like to include a blog with your site, make a ``blog`` directory (or use a path  of your choice, however you'd like it to appear in the URL) with a ``contents.lr`` containing ``_model: blog``.
+   Set the ``sort_key`` field to a suitable integer for it to appear site's main navbar.
+   To add a blog post, create a new subdirectory with the name you'd like for its link, and a ``contents.lr`` with ``_model: blog-post`` inside that.
+
+3. For centralized management of author identities, names and photos, such as for blog posts, create an ``authors`` directory (required name so templates can find it), and a ``contents.lr`` with ``_model: authors`` and ``_discoverable: no`` if you want it to be invisible, or ``_template: page.html`` (or your own custom template) if you want to customize it further and display it to the public.
+   To add an individual author, instantiate a subdirectory named the key by which you'd like to refer to that author elsewhere in the site,.
+   Inside it, place a ``contents.lr`` of ``_model: author`` for the author's details, along with a profile picture (if desired).
+
+4. To create a new general content page, make a new directory with the name you choose for its URL, and save a ``contents.lr`` with ``_model: page``.
+   To make it automatically appear in the main navbar and control its order, set the ``sort_key`` to an appropriate integer.
+   Page layout is relatively free-form, so you can fully control the appearance with simple markdown, embedded HTML or with your own custom stylesheet; more advanced users can extend the page model and build their own complex templates.
+
+5. If you'd like a custom error page rather than whatever your server/host provides by default, create a new directory named ``404.html``, and populate it with a ``contents.lr`` of ``_model: 404`` and ``_discoverable: no``, along with a background image, if desired.
+
+Beyond this, you're free to add flowblocks and content to the main singlepage layout as you desire, along with new blog posts (and their authors), content pages, and even your own custom templates.
+Have fun exploring what you can do!
+
+
+
+## Theme-Level Configuration Settings
+
+While considerable effort has been undertaken to make all practicable settings part of the data models of the relevant pages, to maximize flexibility along with discoverability of settings from the admin GUI and mimize ambiguity and costly full-site re-builds, a few were clearly more appropriate as site-wide options, and this section documents each of them.
+As with any theme, they can be set under the ``[theme_settings]`` section of your projects ``.lektorproject`` configuration file, in ``INI`` format.
+You can view a full example with all settings present with sensible defaults in the ``example-site`` directory of this theme; feel free to use it as a basis for your own.
+
+
+### Interpreted datatypes
+
+Since data types in ``INI`` configuration files are all natively strings by default, Lektor-Icon does some custom parsing on various values to convert them to the indicated type.
+The following describes the various formats and their parsing rules.
+
+*String:* A basic string; no special interpretion.
+Generally converted by ``jinja`` into an HTML-safe representation, escaping problematic characters.
+Used when the output should be plain text.
+
+*Safe String:* Like string, except HTML characters are not escaped (treated as ``safe`` by ``jinja``.
+Therefore, you can do fancy stuff with it like adding complex tags and markup, but be careful!
+You could break the site if you inadvertently include syntactically invalid HTML here.
+
+*URL*: Must be a valid URL, either to a local asset/content (absolute path from the root of the site, e.g. ``/static/images/favicon.png``) or to a full URL remote webpage/resource (``https://www.spyder-ide.org``).
+Exact usage and validation depends on context, so see each setting's description for details.
+
+*CSS Color Code*: Can be any valid value you'd use for a CSS color property, such as hex (``#ff0000``), RGB(A) (``rgb(255, 0, 0)``), HSL(A) (``hsla(0.1, 0.5, 0.7, 0.3)``), or a named color (``red``).
+
+*Boolean:* A setting parsed as a boolean.
+The algorithm is simple: convert the input to a all-lowercase reprisentation, strip any whitespace, and determine if it matches either ``true``/``t``/``yes``/``y``, or ``false``/``f``/``no``/``n``.
+The default (not present or invalid) may be either or even neither; see the description of each setting for details.
+
+*Dictionary:* An ordered dictionary of key-value pairs (each generally treated as string: string or string: URL depending on context), written in Python-style syntax.
+The general form is ``key1: value1, key2: value2`` .
+Any number of spaces (including zero) between commas are allowed, but exactly one must be preset between the colon (``:``) and the value to allow for any colons (like in ``https:``) that might be present in the key or value text.
+
+
+### Theme Settings List
+
+``title`` (*string*): The name of the site, which appears in the second portion of the tab title (i.e. ``<Page Name> | <title>``, as part of the ``<title>`` element for each page.
+*Example*: ``title = Lektor-Icon Demo Site``
+
+``content_lang`` (*string*): ISO 639 language (locale) code of the site's primary content language.
+*Example:* ``content_lang = en-US``
+
+``author`` (*string*): The author of the website.
+Used for the ``"author"`` HTML meta tag.
+*Example:* ``author = Lektor Icon Contributors``
+
+``copyright`` (*string*): The copyright statement of the website.
+Used for the ``"copyright"`` HTML meta tag and the copyright statement in the page footer.
+*Example:* ``copyright = &copy; 2019 Lektor-Icon Contributors``
+
+``description`` (*string*): A short (<1 sentance) summary of the site's purpose.
+Used for the ``"description"`` HTML meta tag.
+*Example:* ``description = Official live demo site for the Lektor-Icon theme``
+
+``keywords`` (*string*): The major keywords describing the website as a comma-separated list.
+Used for the ``"keywords"`` HTML meta tag.
+*Example:* ``keywords = lektor, theme, example``
+
+``favicon_path`` (*url*): The path to the site's primary favicon.
+Typically should be stored in the ``assets/static/`` directory of the source repository, and also repeated (as ``favicon.icon`` in the root of the ``assets`` directory for legacy browsers (i.e. IE).
+*Example:* ``favicon_path = /static/images/favicon.png``
+
+``theme_accent_color`` (*CSS color string*): HTML/CSS color code of the desired accent color for the site's main themed elements.
+Note: Will not be used on legacy Internet Explorer browsers; the default color (Spyder Red) will be used instead unless a workaround is used.
+See Accent Color Theming in the Technical Notes section below for more details.
+*Example:* ``theme_accent_color = #ee1c24``
+
+``theme_pipe_color`` (*CSS color string*): HTML/CSS color code of the desired color for the colored pipe characters used through the site, as well as a few minor elements.
+Generally recommended to be lighter than the primary theme color, for optimal contrast with the dark footer background.
+Note: Will not be used on legacy Internet Explorer browsers; the default color (Spyder Red) will be used instead unless a workaround is used.
+See Accent Color Theming in the Technical Notes section below for more details.
+*Example:* ``theme_pipe_color = rgb(255, 255, 82)``
+
+``custom_css`` (*url*): Path to a custom stylesheet for the site, if desired.
+Loaded last, and thus can be used to customize the look of any desired aspect of the site.
+*Example:* ``custom_css = /static/css/example-custom-styles.css``
+
+``content_security_policy`` (*boolean*): Whether to apply a (relatively liberal) Content Security Policy ruleset to the site, to increase its security.
+If not set, defaults to ``true``.
+Can be further customized with the properties below.
+*Example:* ``content_security_policy = true``
+
+``content_security_policy_frame_src`` (*safe string*): String to use instead of the default for the ``frame_src`` rule of the site's CSP (if enabled).
+*Example:* ``content_security_policy_frame_src = 'self' http: https:``
+
+``content_security_policy_script_src`` (*safe string*): String to use instead of the default for the ``script_src`` rule of the site's CSP (if enabled).
+*Example:* ``content_security_policy_script_src = 'self' 'unsafe-inline'``
+
+``content_security_policy_style_src`` (*safe string*): String to use instead of the default for the ``script_src`` rule of the site's CSP (if enabled).
+*Example:* ``content_security_policy_style_src = 'self' https://fonts.googleapis.com 'unsafe-inline'``
+
+``loader_enable`` (*boolean*): Whether to show the animated loading icon for a brief period while any singlepage-theme page is loading.
+Defaults to true if not set.
+*Example:* ``loader_enable = true``
+
+``nav_logo_path`` (*url*): Path to the logo to use in the top left of the site's main navbar.
+Should generally be somewhere in the site's ``assets`` directory.
+If not present, no logo will be displayed in favor of just ``nav_logo_text``.
+At least of of this and the latter should be present; otherwise no clearly accessible link to the homepage will be easily visible to users (stock text will be generated if this is the case, to avoid this).
+*Example:* ``nav_logo_path = /static/images/spyder-logo.svg``
+
+``nav_logo_text`` (*string*): Text to display as the site's name, next to the logo to the left of the top navbar.
+If not present, no text will be displayed unless ``nav_logo_path`` also doesn't exist; in that case stock text will be displayed to allow users/you to at least navigate the site properly.
+*Example:* ``nav_logo_text = Lektor-Icon``
+
+``nav_extralinks`` (*dictionary*): Dictionary (written in Python style, see above) of extra navigation links to show to the right of the top navbar.
+Will be displayed to the right of any other links, in the order listed here.
+These can be to other parts or subdomains of your site (generated separately), sites on other platforms (e.g. Github), or third-party resources.
+Key is the link title, and value is the link URL.
+If not present, no extra links will be displayed.
+*Example:* ``nav_extralinks = Spyder: https://www.spyder-ide.org/, Github: https://github.com/spyder-ide/lektor-icon``
+
+``footer_links`` (*dictionary*): Dictionary (written in Python style, see above) of "Connect with Us:" links to be displayed at the top of the site's footer.
+You may want to include links to various social media, hosting and/or fundraising platforms here.
+Key is the link title, and value is the link URL.
+If not present, no "Connect with Us" section of the footer will be displayed.
+*Example:* ``footer_links = Github: https://github.com/spyder-ide/lektor-icon, Facebook: https://www.facebook.com/SpyderIDE/, Twitter: https://twitter.com/spyder_ide, Google Groups: https://groups.google.com/forum/?hl=en#!forum/uahuntsvilleams, OpenCollective: https://opencollective.com/spyder``
+
+``footer_license_name`` (*string*): The name of your site's content license(s).
+Will be displayed next to ``copyright`` (see above), and linked to the URL below if present.
+If not present, no license text will be displayed.
+*Example:* ``footer_license_name = MIT (Code); CC-BY-SA 4.0 (Content)``
+
+``footer_license_link`` (*url*): Link to your site's license, either in its canonical form/summary (e.g. on the Creative Commons website), or e.g. the ``LICENSE.txt`` file from your repo itself.
+If not present, the license text will not be linked.
+*Example:* ``footer_license_link = https://github.com/spyder-ide/lektor-icon/blob/master/LICENSE.txt``
+
+``gitter_room`` (*string*): Name/path to the Gitter room associated with your site (the part of its Gitter URL after ``https://gitter.im/``).
+If included, will enable an ``Open Chat`` button on your site that, when clicked, will open a fully interactive sidebar containing a Gitter chat client, allowing them to communicate directly with you to ask questions or give feedback.
+If not set, the script will not be enabled and no button will be present.
+See the Optional Feature Configuration section for more details.
+*Example:* ``gitter_room = spyder-ide/public``
+
+``disqus_name`` (*string*): The name of your Disqus organization on their website.
+If set, will globally allow Disqus comments on blogs on your site.
+Each individual blog's default (comments on or off) can be set on its configuration page.
+Furthermore, this can again be overrided on a per-post basis and comments individually set to on or off.
+Also, the ``lektor-disqus-comments`` plugin needs to be installed, and the ``disqus-comments.ini`` file needs to be present in the ``config`` directory of your site for it to work.
+If not present, comments will be globally disabled across your site.
+See the Optional Feature Configuration section for more details.
+*Example:* ``disqus_name = spyder-ide``
+
+``atom_enable`` (*boolean*): Whether to enable generating and displaying a RSS/Atom feed for the site's blog.
+Requires the ``lektor-atom`` plugin and using the ``--no-prune`` flag on build, and ``atom.ini`` file needs to be present in the ``config`` directory of your site for it to work.
+See the Optional Feature Configuration section for more details.
+*Example:* ``atom_enable = true``
+
+
+
+## Optional Feature Configuration
+
+Lektor-Icon includes a number of features and integrations that can be optionally enabled if you so desire, that each require a bit of minimal configuration:
+
+
+### Gitter Chat Sidebar
+
+[Gitter](https://gitter.im/) provides free public and private chatrooms for open-source projects hosted on public repositories, and the Lektor-Icon theme includes an optional button that activates a slide-open chat panel that users of your site can use to ask questions, give feedback, and otherwise interact with you and your organization.
+Configuration on this one is simple: All you need to do is enter the name/path of your Gitter room (i.e. the part that appears after ``https://gitter.im/`` in its URL) in the ``.lektorproject [theme_settings]`` and you should see the chat button appear throughout your site.
+
+
+### Disqus Blog Comments
+
+[Disqus](https://disqus.com/) is a user-interaction platform that can be embedded in websites to allow users to comment on articles and blog posts, and its a built-in option for Lektor-Icon's blog.
+To enable it, you need to first create your free (gratis) Disqus community at [Disqus Engage](https://publishers.disqus.com/engage), and register your community name.
+Then, you'll need to create a new file under ``configs/disqus-comments.ini`` with ``shortname = YOUR_DISQUS_SHORTNAME`` as the content, include the shortname under ``disqus_name`` in the ``[theme_settings]`` in your ``.lektorproject`` (to enable it in the theme and allow Disqus through CSP), and add ``lektor-disqus-comments = 0.2`` under ``[packages]`` there as well.
+Finally, you can either enable comments by default for all posts posts for the blog by setting the ``allow_comments_default`` field to ``yes`` in the Blog configuration page, or you can enable/disable comments for individual blogs with the ``allow_comments`` field on each one.
+Check out the [Disqus Comments](https://www.getlektor.com/docs/guides/disqus/) tutorial in the Lektor docs to learn more.
+
+
+### RSS/Atom Feed for Blog Posts
+
+RSS/Atom feeds, while less common then they used to be, are still sometimes desired by website owners and blog aggregators (e.g. the various *Planet sites) for reading and collating content. Accordingly, Lektor-Icon has support for it as part of its blog template.
+To enable it, you'll want to set ``atom_enable`` to ``true``/``yes`` in the ``[theme_settings]``, add ``lektor-atom = 0.3`` under ``[packages]`` there, and set ``url_style = absolute`` and ``url = https://www.example.com`` under the ``[project]`` section so the feed URL works.
+You'll also need to set up a ``configs/atom.ini`` configuration file, with the content
+
+```ini
+[main]
+name = Name Your Feed
+source_path = /blog
+```
+
+where ``/blog`` is the path to your blog, and set the ``--no-prune`` flag for ``lektor build`` so your ``feed.xml`` file is not removed.
+
+
+
+## Link Organization and Navigation
+
+The main navbar contains three distinct categories of links, two of which are present on every page, and each of which are controlled and ordered through different methods.
+All of these, when present, are found both stickied at the top of the screen with the standard desktop/widescreen layout, and listed in the sidebar under the "hamburger" menu in the mobile/portrait layout.
+
+
+### Page Content Links
+
+First, from the left, on the single-page layout only, are the section links automatically generated for any top-level flowblock included on the page, in the order present on the page with a user-customizable display title and section identifier allowing for copying and pasting; when clicked, they smoothly scroll the browser to the named section.
+You can control the link text, as well as whether it appears it all, on a per-section/flowblock basis using the ``nav_link`` field.
+Optionally, a link back to the hero image ("Home") can also be included.
+Obviously, these are not present on non-single-page layouts, such as the blog or individual pages.
+However, if enabled (see above), the RSS link appears here on the blog pages.
+
+
+### Subpage Links
+
+Second, present on every page, are the links automatically generated to top-level subpages of the index page meeting certain criteria.
+Specifically, for this section the template searches the root of the content directory of the site for any top-level directories with content that is both discoverable (i.e. it has the ``_discoverable`` field set to ``yes``, or is discoverable by default), and has a ``sort_key`` set, and sorts them from left to right by ascending ``sort_key``.
+Currently, models with these fields include the blog, as well as any based off the individual page model, except for ``404.html`` (for obvious reasons).
+The displayed link title for each is the ``short_title`` field of the page (also used for the page tab title) if present, and otherwise a "prettified" version of the page's filename (converted to title case, with path characters removed and underscores/hyphens replaced with spaces) is employed.
+
+
+### Custom Links
+
+Finally, you can add your own custom internal or external links (e.g. to a subdomain with your project's documentation, generated by a different CMS, or to a site hosted on another platform like Github, ReadTheDocs and the like) to the theme options, which will be displayed in the order you list them in, and with the link titles you choose; see the Theme Settings section for more on configuring them.
+These will wrap with the automatically subpage links, and be included on every page. At present, due to the relatively "flat" nature of sites typically built with this theme, there are not currently built-in facilities for automatically including the links of child pages in a context-dependent manner, but that would be relatively straightforward to add if sufficient interest exists.
+
+
+
+## Technical Notes
+
+### Responsive Image Handling
+
+Most images, except for static assets like the site's logo and favicon, are automatically resized in HTML or CSS for optimal display and efficiency on a variety of platforms.
+Browsers that don't support responsive images will gracefully degrade to a reasonable default image (generally sized for 1080p desktops and larger non-Retina mobile devices).
+Therefore, there is no harm in providing the highest reasonable resolution available for each image (nominally ~3840 px width for full-page backgrounds, ~1280px for other images) and the theme will automatically resize the images and, in many cases, send a version specifically optimized for the reader's screen size.
+
+
+### Accent Color Theming
+
+You can also configure the "pipe" color, which is used on the pipe separators as well as the RSS button (if enabled) and is suggested to be a lighter variation of the primary accent color (since it will be heavily used in the footer, with a relatively dark background).
+By default, the former is set to "Spyder Red" (``#EE1C24``), and the latter is the lighter red ``#FF4C52``, but you can set them to anything you want.
+
+**Important Note:** Since Internet Explorer 11 (along with Edge <15, Safari <9.1, iOS <9.3 and Android browser <= 4.4, plus pre-2014 Firefox and pre-2016 Chrome and Opera) do not support CSS variables, which is how this feature is implemented, it will fall back to the default color for each.
+Therefore, users of your site running legacy browsers will see the various themed accent elements in that default color rather than your custom one, if you've set one up.
+As there is no straightforward way to implement this without CSS variables, modifying the theme for each color or tons of unmaintainable inline styles, you have a few choices to work around this:
+
+* Use the default accent color (great if it fits your site, but obviously limits your creative flexibility).
+* Accept that your users remaining on IE11 and other legacy browsers will see your site with the default accent color, and advise they upgrade to a modern browser (IE market share is now <3% globally and continues to drop, so this will eventually become the preferred approach).
+* Do a find and replace in ``style.css`` for the above two colors, and commit your changes to a modified branch of this theme (relatively quick and straightforward, but must be re-applied if you update to a newer version of this theme).
+* Include rules in your custom stylesheet (which you can set in ``[theme_settings]``) to replace each color on our stylesheet with your hardcoded preferred one (potentially a lot of work and could cause style bugs if not done correctly, but doesn't require modifying the theme itself and thus re-applying your changes on pulling a fresh version).
+
+
+### Browser Compatibility
+
+This theme has been tested for full functionality and consistent layout across January 2019 official releases of the "big four" modern desktop browsers, Firefox, Chrome, and Edge, as well as the legacy Internet Explorer 11 (with the aforementioned custom accent color exception).
+Aside from a modest number of progressive enhancements (and the custom accent color), the layout, styles and functionality should render identically or near-identically in desktop and mobile versions of Chrome, Opera, and Firefox from at least 2015-onwards, as well as Safari 9+, any version of Edge, iOS 9.3+, Android browser >4.4, and the aforementioned IE11.
+It should degrade relatively gracefully (if at all) with all core functionality and layout intact on 2013+ releases of Firefox, Chrome and Opera, along with iOS 9.x and Android browser 4.4 (amounting to over >95-98% of all users), although without real-world testing this is by no means guaranteed, and users should be encouraged to upgrade to a modern version of one of the aforementioned browsers.
+The primary blocker for browsers older than this is flexbox support, which several elements rely upon for fully correct layout.
+
+
+### Library Versions and Security
+
+Lektor-Icon has been updated with the latest (as of Feburary 1, 2019) releases of jQuery (3.3.1) and all other included libraries/plugins, with some additional manual patches applied to fix deprecations and other Javascript warnings and errors.
+The theme originally relied on Bootstrap 3.3.5 along with a number of other major third party stylesheets, JavaScript libraries and fonts but that is no longer the case, the JS is no longer used and the relatively few CSS styles utilized were inlined into the main stylesheet.
+
+Although destined for relatively low-risk static site applications, the remaining libraries have been checked for unpatched vulnerabilities and considerable security hardening of the headers, links and elements have been done.
+Potentially burdensome restrictions have been been avoided by default, while allowing further user configuration of the CSP and other headers through the theme options.
+
+
+
+## Contributing
+
+Want to add a feature, report a bug, or suggest an enhancement to this theme?
+You can—everyone is welcome to help with Lektor-Icon!
+Please read our [contributing guide](https://github.com/spyder-ide/lektor-icon/blob/master/CONTRIBUTING.md) to get started!
+Thanks for your interest in Lektor-Icon, and we appreciate everyone using and supporting the project!
+
+
+
+## License
+
+The theme code as a whole is distributed under the [MIT (Expat) license](https://opensource.org/licenses/MIT), and certain portions are also originally licensed [Creative Commons Attribution 3.0](https://creativecommons.org/licenses/by/3.0/) (CC-BY-3.0), along with certain other third-party external assets included under other permissive licenses.
+You're covered and don't need to do anything else so long as the fine print in the footer is displayed, and a copy of the ``LICENSE.txt`` is preserved in the theme repo; if you don't modify the theme itself both conditions will always be fulfilled.
+For all the gory legal details, see the [LICENSE.txt](https://github.com/spyder-ide/lektor-icon/blob/master/LICENSE.txt) and [NOTICE.txt](https://github.com/spyder-ide/lektor-icon/blob/master/NOTICE.txt) in the root of the theme repository.
+
+The contents of the ``example-site`` directory are distributed under separate terms, the [Creative Commons Attribution Share-Alike 4.0 International](https://creativecommons.org/licenses/by-sa/4.0/) (CC-BY-SA 4.0) license, so see the [README in that directory](https://github.com/spyder-ide/lektor-icon/blob/master/example-site/README.md) for more details.
+
+
+
+## Credits
+
+The original [plain HTML5 template](https://freehtml5.co/icon-free-website-template-using-bootstrap/) which was the basis for Hugo-Icon, and in turn Lektor-Icon was created by [FreeHTML5.co](https://freehtml5.co/) and released under the [Creative Commons Attribution 3.0](https://creativecommons.org/licenses/by/3.0/) license.
+Attribution is built into the rendered theme's common footer, so please retain it, as well as this credits section in a visible place in the source code (like here in the README) and the proper legal notices in the LICENSE.txt and NOTICE.txt files (see above section).
+
+The [Hugo port](https://github.com/SteveLane/hugo-icon) of the theme, the source for this Lektor version, was created by [Steve Lane](https://gtown-ds.netlify.com/), with the modifications released under the [MIT (Expat) License](https://github.com/SteveLane/hugo-icon/blob/master/LICENSE.md).
+
+This Lektor port was created by [Daniel Althviz](https://dalthviz.github.io/), and maintained and further developed by the [Spyder organization](https://github.com/spyder-ide/), and also distributed under the [MIT license](https://github.com/spyder-ide/lektor-icon/blob/master/LICENSE.txt).
+It is used for the [official website](https://www.spyder-ide.org/) of Spyder, the Scientific Python Development Environment.
+
+
+
+## Changelog
+
+A [changelog](https://github.com/spyder-ide/lektor-icon/blob/master/changelog.md) for this theme lists the major modifications and improvements since the initial port to Hugo by @SteveLane; if you fork this theme and make changes, please list them.
+
+
+
+## About the Spyder IDE
+
+Spyder is a powerful scientific environment written in Python, for Python, and designed by and for scientists, engineers and data analysts.
+It offers a unique combination of the advanced editing, analysis, debugging, and profiling functionality of a comprehensive development tool with the data exploration, interactive execution, deep inspection, and beautiful visualization capabilities of a scientific package.
+
+Beyond its many built-in features, its abilities can be extended even further via its plugin system and API.
+Furthermore, Spyder can also be used as a PyQt5 extension library, allowing you to build upon its functionality and embed its components, such as the interactive console, in your own software.
+
+For more general information about Spyder and to stay up to date on the latest Spyder news and information, please check out [our website](https://www.spyder-ide.org/).
+
+
+
+## More information
+
+[Spyder Website](https://www.spyder-ide.org/)
+
+[Download Spyder (with Anaconda)](https://www.anaconda.com/download/)
+
+[Spyder Github](https://github.com/spyder-ide/spyder)
+
+[Gitter Chatroom](https://gitter.im/spyder-ide/public)
+
+[Google Group](https://groups.google.com/group/spyderlib)
+
+[@Spyder_IDE on Twitter](https://twitter.com/spyder_ide)
+
+[@SpyderIDE on Facebook](https://www.facebook.com/SpyderIDE/)
+
+[Support Spyder on OpenCollective](https://opencollective.com/spyder/)
diff --git a/website/themes/lektor-icon/assets/static/css/bootstrap.css b/website/themes/lektor-icon/assets/static/css/bootstrap.css
new file mode 100644
index 0000000..d7274c5
--- /dev/null
+++ b/website/themes/lektor-icon/assets/static/css/bootstrap.css
@@ -0,0 +1,7322 @@
+@charset "UTF-8";
+
+/*!
+ * Bootstrap v3.3.5 (https://getbootstrap.com)
+ * Copyright 2011-2015 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+html {
+  font-family: sans-serif;
+  -ms-text-size-adjust: 100%;
+  -webkit-text-size-adjust: 100%;
+}
+
+body {
+  margin: 0;
+}
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+menu,
+nav,
+section,
+summary {
+  display: block;
+}
+
+audio,
+canvas,
+progress,
+video {
+  display: inline-block;
+  vertical-align: baseline;
+}
+
+audio:not([controls]) {
+  display: none;
+  height: 0;
+}
+
+[hidden],
+template {
+  display: none;
+}
+
+a {
+  background-color: transparent;
+}
+
+a:active,
+a:hover {
+  outline: 0;
+}
+
+abbr[title] {
+  border-bottom: 1px dotted;
+}
+
+b,
+strong {
+  font-weight: bold;
+}
+
+dfn {
+  font-style: italic;
+}
+
+h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+
+mark {
+  background: #ff0;
+  color: #000;
+}
+
+small {
+  font-size: 80%;
+}
+
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+sup {
+  top: -0.5em;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+img {
+  border: 0;
+}
+
+svg:not(:root) {
+  overflow: hidden;
+}
+
+figure {
+  margin: 1em 40px;
+}
+
+hr {
+  box-sizing: content-box;
+  height: 0;
+}
+
+pre {
+  overflow: auto;
+}
+
+code,
+kbd,
+pre,
+samp {
+  font-family: monospace, monospace;
+  font-size: 1em;
+}
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  color: inherit;
+  font: inherit;
+  margin: 0;
+}
+
+button {
+  overflow: visible;
+}
+
+button,
+select {
+  text-transform: none;
+}
+
+button,
+html input[type="button"],
+input[type="reset"],
+input[type="submit"] {
+  -webkit-appearance: button;
+  cursor: pointer;
+}
+
+button[disabled],
+html input[disabled] {
+  cursor: default;
+}
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+  border: 0;
+  padding: 0;
+}
+
+input {
+  line-height: normal;
+}
+
+input[type="checkbox"],
+input[type="radio"] {
+  box-sizing: border-box;
+  padding: 0;
+}
+
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+input[type="search"] {
+  -webkit-appearance: textfield;
+  box-sizing: content-box;
+}
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+fieldset {
+  border: 1px solid #c0c0c0;
+  margin: 0 2px;
+  padding: 0.35em 0.625em 0.75em;
+}
+
+legend {
+  border: 0;
+  padding: 0;
+}
+
+textarea {
+  overflow: auto;
+}
+
+optgroup {
+  font-weight: bold;
+}
+
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+
+td,
+th {
+  padding: 0;
+}
+
+/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */
+@media print {
+  *,
+  *:before,
+  *:after {
+    background: transparent !important;
+    color: #000 !important;
+    box-shadow: none !important;
+    text-shadow: none !important;
+  }
+
+  a,
+  a:visited {
+    text-decoration: underline;
+  }
+
+  a[href]:after {
+    content: " (" attr(href) ")";
+  }
+
+  abbr[title]:after {
+    content: " (" attr(title) ")";
+  }
+
+  a[href^="#"]:after,
+  a[href^="javascript:"]:after {
+    content: "";
+  }
+
+  pre,
+  blockquote {
+    border: 1px solid #999;
+    page-break-inside: avoid;
+  }
+
+  thead {
+    display: table-header-group;
+  }
+
+  tr,
+  img {
+    page-break-inside: avoid;
+  }
+
+  img {
+    max-width: 100% !important;
+  }
+
+  p,
+  h2,
+  h3 {
+    orphans: 3;
+    widows: 3;
+  }
+
+  h2,
+  h3 {
+    page-break-after: avoid;
+  }
+
+  .navbar {
+    display: none;
+  }
+
+  .btn > .caret,
+  .dropup > .btn > .caret {
+    border-top-color: #000 !important;
+  }
+
+  .label {
+    border: 1px solid #000;
+  }
+
+  .table {
+    border-collapse: collapse !important;
+  }
+  .table td,
+  .table th {
+    background-color: #fff !important;
+  }
+
+  .table-bordered th,
+  .table-bordered td {
+    border: 1px solid #ddd !important;
+  }
+}
+@font-face {
+  font-family: 'Glyphicons Halflings';
+  src: url("../fonts/bootstrap/glyphicons-halflings-regular.woff2") format("woff2"), url("../fonts/bootstrap/glyphicons-halflings-regular.woff") format("woff");
+}
+.glyphicon {
+  position: relative;
+  top: 1px;
+  display: inline-block;
+  font-family: 'Glyphicons Halflings';
+  font-style: normal;
+  font-weight: normal;
+  line-height: 1;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.glyphicon-asterisk:before {
+  content: "\2a";
+}
+
+.glyphicon-plus:before {
+  content: "\2b";
+}
+
+.glyphicon-euro:before,
+.glyphicon-eur:before {
+  content: "\20ac";
+}
+
+.glyphicon-minus:before {
+  content: "\2212";
+}
+
+.glyphicon-cloud:before {
+  content: "\2601";
+}
+
+.glyphicon-envelope:before {
+  content: "\2709";
+}
+
+.glyphicon-pencil:before {
+  content: "\270f";
+}
+
+.glyphicon-glass:before {
+  content: "\e001";
+}
+
+.glyphicon-music:before {
+  content: "\e002";
+}
+
+.glyphicon-search:before {
+  content: "\e003";
+}
+
+.glyphicon-heart:before {
+  content: "\e005";
+}
+
+.glyphicon-star:before {
+  content: "\e006";
+}
+
+.glyphicon-star-empty:before {
+  content: "\e007";
+}
+
+.glyphicon-user:before {
+  content: "\e008";
+}
+
+.glyphicon-film:before {
+  content: "\e009";
+}
+
+.glyphicon-th-large:before {
+  content: "\e010";
+}
+
+.glyphicon-th:before {
+  content: "\e011";
+}
+
+.glyphicon-th-list:before {
+  content: "\e012";
+}
+
+.glyphicon-ok:before {
+  content: "\e013";
+}
+
+.glyphicon-remove:before {
+  content: "\e014";
+}
+
+.glyphicon-zoom-in:before {
+  content: "\e015";
+}
+
+.glyphicon-zoom-out:before {
+  content: "\e016";
+}
+
+.glyphicon-off:before {
+  content: "\e017";
+}
+
+.glyphicon-signal:before {
+  content: "\e018";
+}
+
+.glyphicon-cog:before {
+  content: "\e019";
+}
+
+.glyphicon-trash:before {
+  content: "\e020";
+}
+
+.glyphicon-home:before {
+  content: "\e021";
+}
+
+.glyphicon-file:before {
+  content: "\e022";
+}
+
+.glyphicon-time:before {
+  content: "\e023";
+}
+
+.glyphicon-road:before {
+  content: "\e024";
+}
+
+.glyphicon-download-alt:before {
+  content: "\e025";
+}
+
+.glyphicon-download:before {
+  content: "\e026";
+}
+
+.glyphicon-upload:before {
+  content: "\e027";
+}
+
+.glyphicon-inbox:before {
+  content: "\e028";
+}
+
+.glyphicon-play-circle:before {
+  content: "\e029";
+}
+
+.glyphicon-repeat:before {
+  content: "\e030";
+}
+
+.glyphicon-refresh:before {
+  content: "\e031";
+}
+
+.glyphicon-list-alt:before {
+  content: "\e032";
+}
+
+.glyphicon-lock:before {
+  content: "\e033";
+}
+
+.glyphicon-flag:before {
+  content: "\e034";
+}
+
+.glyphicon-headphones:before {
+  content: "\e035";
+}
+
+.glyphicon-volume-off:before {
+  content: "\e036";
+}
+
+.glyphicon-volume-down:before {
+  content: "\e037";
+}
+
+.glyphicon-volume-up:before {
+  content: "\e038";
+}
+
+.glyphicon-qrcode:before {
+  content: "\e039";
+}
+
+.glyphicon-barcode:before {
+  content: "\e040";
+}
+
+.glyphicon-tag:before {
+  content: "\e041";
+}
+
+.glyphicon-tags:before {
+  content: "\e042";
+}
+
+.glyphicon-book:before {
+  content: "\e043";
+}
+
+.glyphicon-bookmark:before {
+  content: "\e044";
+}
+
+.glyphicon-print:before {
+  content: "\e045";
+}
+
+.glyphicon-camera:before {
+  content: "\e046";
+}
+
+.glyphicon-font:before {
+  content: "\e047";
+}
+
+.glyphicon-bold:before {
+  content: "\e048";
+}
+
+.glyphicon-italic:before {
+  content: "\e049";
+}
+
+.glyphicon-text-height:before {
+  content: "\e050";
+}
+
+.glyphicon-text-width:before {
+  content: "\e051";
+}
+
+.glyphicon-align-left:before {
+  content: "\e052";
+}
+
+.glyphicon-align-center:before {
+  content: "\e053";
+}
+
+.glyphicon-align-right:before {
+  content: "\e054";
+}
+
+.glyphicon-align-justify:before {
+  content: "\e055";
+}
+
+.glyphicon-list:before {
+  content: "\e056";
+}
+
+.glyphicon-indent-left:before {
+  content: "\e057";
+}
+
+.glyphicon-indent-right:before {
+  content: "\e058";
+}
+
+.glyphicon-facetime-video:before {
+  content: "\e059";
+}
+
+.glyphicon-picture:before {
+  content: "\e060";
+}
+
+.glyphicon-map-marker:before {
+  content: "\e062";
+}
+
+.glyphicon-adjust:before {
+  content: "\e063";
+}
+
+.glyphicon-tint:before {
+  content: "\e064";
+}
+
+.glyphicon-edit:before {
+  content: "\e065";
+}
+
+.glyphicon-share:before {
+  content: "\e066";
+}
+
+.glyphicon-check:before {
+  content: "\e067";
+}
+
+.glyphicon-move:before {
+  content: "\e068";
+}
+
+.glyphicon-step-backward:before {
+  content: "\e069";
+}
+
+.glyphicon-fast-backward:before {
+  content: "\e070";
+}
+
+.glyphicon-backward:before {
+  content: "\e071";
+}
+
+.glyphicon-play:before {
+  content: "\e072";
+}
+
+.glyphicon-pause:before {
+  content: "\e073";
+}
+
+.glyphicon-stop:before {
+  content: "\e074";
+}
+
+.glyphicon-forward:before {
+  content: "\e075";
+}
+
+.glyphicon-fast-forward:before {
+  content: "\e076";
+}
+
+.glyphicon-step-forward:before {
+  content: "\e077";
+}
+
+.glyphicon-eject:before {
+  content: "\e078";
+}
+
+.glyphicon-chevron-left:before {
+  content: "\e079";
+}
+
+.glyphicon-chevron-right:before {
+  content: "\e080";
+}
+
+.glyphicon-plus-sign:before {
+  content: "\e081";
+}
+
+.glyphicon-minus-sign:before {
+  content: "\e082";
+}
+
+.glyphicon-remove-sign:before {
+  content: "\e083";
+}
+
+.glyphicon-ok-sign:before {
+  content: "\e084";
+}
+
+.glyphicon-question-sign:before {
+  content: "\e085";
+}
+
+.glyphicon-info-sign:before {
+  content: "\e086";
+}
+
+.glyphicon-screenshot:before {
+  content: "\e087";
+}
+
+.glyphicon-remove-circle:before {
+  content: "\e088";
+}
+
+.glyphicon-ok-circle:before {
+  content: "\e089";
+}
+
+.glyphicon-ban-circle:before {
+  content: "\e090";
+}
+
+.glyphicon-arrow-left:before {
+  content: "\e091";
+}
+
+.glyphicon-arrow-right:before {
+  content: "\e092";
+}
+
+.glyphicon-arrow-up:before {
+  content: "\e093";
+}
+
+.glyphicon-arrow-down:before {
+  content: "\e094";
+}
+
+.glyphicon-share-alt:before {
+  content: "\e095";
+}
+
+.glyphicon-resize-full:before {
+  content: "\e096";
+}
+
+.glyphicon-resize-small:before {
+  content: "\e097";
+}
+
+.glyphicon-exclamation-sign:before {
+  content: "\e101";
+}
+
+.glyphicon-gift:before {
+  content: "\e102";
+}
+
+.glyphicon-leaf:before {
+  content: "\e103";
+}
+
+.glyphicon-fire:before {
+  content: "\e104";
+}
+
+.glyphicon-eye-open:before {
+  content: "\e105";
+}
+
+.glyphicon-eye-close:before {
+  content: "\e106";
+}
+
+.glyphicon-warning-sign:before {
+  content: "\e107";
+}
+
+.glyphicon-plane:before {
+  content: "\e108";
+}
+
+.glyphicon-calendar:before {
+  content: "\e109";
+}
+
+.glyphicon-random:before {
+  content: "\e110";
+}
+
+.glyphicon-comment:before {
+  content: "\e111";
+}
+
+.glyphicon-magnet:before {
+  content: "\e112";
+}
+
+.glyphicon-chevron-up:before {
+  content: "\e113";
+}
+
+.glyphicon-chevron-down:before {
+  content: "\e114";
+}
+
+.glyphicon-retweet:before {
+  content: "\e115";
+}
+
+.glyphicon-shopping-cart:before {
+  content: "\e116";
+}
+
+.glyphicon-folder-close:before {
+  content: "\e117";
+}
+
+.glyphicon-folder-open:before {
+  content: "\e118";
+}
+
+.glyphicon-resize-vertical:before {
+  content: "\e119";
+}
+
+.glyphicon-resize-horizontal:before {
+  content: "\e120";
+}
+
+.glyphicon-hdd:before {
+  content: "\e121";
+}
+
+.glyphicon-bullhorn:before {
+  content: "\e122";
+}
+
+.glyphicon-bell:before {
+  content: "\e123";
+}
+
+.glyphicon-certificate:before {
+  content: "\e124";
+}
+
+.glyphicon-thumbs-up:before {
+  content: "\e125";
+}
+
+.glyphicon-thumbs-down:before {
+  content: "\e126";
+}
+
+.glyphicon-hand-right:before {
+  content: "\e127";
+}
+
+.glyphicon-hand-left:before {
+  content: "\e128";
+}
+
+.glyphicon-hand-up:before {
+  content: "\e129";
+}
+
+.glyphicon-hand-down:before {
+  content: "\e130";
+}
+
+.glyphicon-circle-arrow-right:before {
+  content: "\e131";
+}
+
+.glyphicon-circle-arrow-left:before {
+  content: "\e132";
+}
+
+.glyphicon-circle-arrow-up:before {
+  content: "\e133";
+}
+
+.glyphicon-circle-arrow-down:before {
+  content: "\e134";
+}
+
+.glyphicon-globe:before {
+  content: "\e135";
+}
+
+.glyphicon-wrench:before {
+  content: "\e136";
+}
+
+.glyphicon-tasks:before {
+  content: "\e137";
+}
+
+.glyphicon-filter:before {
+  content: "\e138";
+}
+
+.glyphicon-briefcase:before {
+  content: "\e139";
+}
+
+.glyphicon-fullscreen:before {
+  content: "\e140";
+}
+
+.glyphicon-dashboard:before {
+  content: "\e141";
+}
+
+.glyphicon-paperclip:before {
+  content: "\e142";
+}
+
+.glyphicon-heart-empty:before {
+  content: "\e143";
+}
+
+.glyphicon-link:before {
+  content: "\e144";
+}
+
+.glyphicon-phone:before {
+  content: "\e145";
+}
+
+.glyphicon-pushpin:before {
+  content: "\e146";
+}
+
+.glyphicon-usd:before {
+  content: "\e148";
+}
+
+.glyphicon-gbp:before {
+  content: "\e149";
+}
+
+.glyphicon-sort:before {
+  content: "\e150";
+}
+
+.glyphicon-sort-by-alphabet:before {
+  content: "\e151";
+}
+
+.glyphicon-sort-by-alphabet-alt:before {
+  content: "\e152";
+}
+
+.glyphicon-sort-by-order:before {
+  content: "\e153";
+}
+
+.glyphicon-sort-by-order-alt:before {
+  content: "\e154";
+}
+
+.glyphicon-sort-by-attributes:before {
+  content: "\e155";
+}
+
+.glyphicon-sort-by-attributes-alt:before {
+  content: "\e156";
+}
+
+.glyphicon-unchecked:before {
+  content: "\e157";
+}
+
+.glyphicon-expand:before {
+  content: "\e158";
+}
+
+.glyphicon-collapse-down:before {
+  content: "\e159";
+}
+
+.glyphicon-collapse-up:before {
+  content: "\e160";
+}
+
+.glyphicon-log-in:before {
+  content: "\e161";
+}
+
+.glyphicon-flash:before {
+  content: "\e162";
+}
+
+.glyphicon-log-out:before {
+  content: "\e163";
+}
+
+.glyphicon-new-window:before {
+  content: "\e164";
+}
+
+.glyphicon-record:before {
+  content: "\e165";
+}
+
+.glyphicon-save:before {
+  content: "\e166";
+}
+
+.glyphicon-open:before {
+  content: "\e167";
+}
+
+.glyphicon-saved:before {
+  content: "\e168";
+}
+
+.glyphicon-import:before {
+  content: "\e169";
+}
+
+.glyphicon-export:before {
+  content: "\e170";
+}
+
+.glyphicon-send:before {
+  content: "\e171";
+}
+
+.glyphicon-floppy-disk:before {
+  content: "\e172";
+}
+
+.glyphicon-floppy-saved:before {
+  content: "\e173";
+}
+
+.glyphicon-floppy-remove:before {
+  content: "\e174";
+}
+
+.glyphicon-floppy-save:before {
+  content: "\e175";
+}
+
+.glyphicon-floppy-open:before {
+  content: "\e176";
+}
+
+.glyphicon-credit-card:before {
+  content: "\e177";
+}
+
+.glyphicon-transfer:before {
+  content: "\e178";
+}
+
+.glyphicon-cutlery:before {
+  content: "\e179";
+}
+
+.glyphicon-header:before {
+  content: "\e180";
+}
+
+.glyphicon-compressed:before {
+  content: "\e181";
+}
+
+.glyphicon-earphone:before {
+  content: "\e182";
+}
+
+.glyphicon-phone-alt:before {
+  content: "\e183";
+}
+
+.glyphicon-tower:before {
+  content: "\e184";
+}
+
+.glyphicon-stats:before {
+  content: "\e185";
+}
+
+.glyphicon-sd-video:before {
+  content: "\e186";
+}
+
+.glyphicon-hd-video:before {
+  content: "\e187";
+}
+
+.glyphicon-subtitles:before {
+  content: "\e188";
+}
+
+.glyphicon-sound-stereo:before {
+  content: "\e189";
+}
+
+.glyphicon-sound-dolby:before {
+  content: "\e190";
+}
+
+.glyphicon-sound-5-1:before {
+  content: "\e191";
+}
+
+.glyphicon-sound-6-1:before {
+  content: "\e192";
+}
+
+.glyphicon-sound-7-1:before {
+  content: "\e193";
+}
+
+.glyphicon-copyright-mark:before {
+  content: "\e194";
+}
+
+.glyphicon-registration-mark:before {
+  content: "\e195";
+}
+
+.glyphicon-cloud-download:before {
+  content: "\e197";
+}
+
+.glyphicon-cloud-upload:before {
+  content: "\e198";
+}
+
+.glyphicon-tree-conifer:before {
+  content: "\e199";
+}
+
+.glyphicon-tree-deciduous:before {
+  content: "\e200";
+}
+
+.glyphicon-cd:before {
+  content: "\e201";
+}
+
+.glyphicon-save-file:before {
+  content: "\e202";
+}
+
+.glyphicon-open-file:before {
+  content: "\e203";
+}
+
+.glyphicon-level-up:before {
+  content: "\e204";
+}
+
+.glyphicon-copy:before {
+  content: "\e205";
+}
+
+.glyphicon-paste:before {
+  content: "\e206";
+}
+
+.glyphicon-alert:before {
+  content: "\e209";
+}
+
+.glyphicon-equalizer:before {
+  content: "\e210";
+}
+
+.glyphicon-king:before {
+  content: "\e211";
+}
+
+.glyphicon-queen:before {
+  content: "\e212";
+}
+
+.glyphicon-pawn:before {
+  content: "\e213";
+}
+
+.glyphicon-bishop:before {
+  content: "\e214";
+}
+
+.glyphicon-knight:before {
+  content: "\e215";
+}
+
+.glyphicon-baby-formula:before {
+  content: "\e216";
+}
+
+.glyphicon-tent:before {
+  content: "\26fa";
+}
+
+.glyphicon-blackboard:before {
+  content: "\e218";
+}
+
+.glyphicon-bed:before {
+  content: "\e219";
+}
+
+.glyphicon-apple:before {
+  content: "\f8ff";
+}
+
+.glyphicon-erase:before {
+  content: "\e221";
+}
+
+.glyphicon-hourglass:before {
+  content: "\231b";
+}
+
+.glyphicon-lamp:before {
+  content: "\e223";
+}
+
+.glyphicon-duplicate:before {
+  content: "\e224";
+}
+
+.glyphicon-piggy-bank:before {
+  content: "\e225";
+}
+
+.glyphicon-scissors:before {
+  content: "\e226";
+}
+
+.glyphicon-bitcoin:before {
+  content: "\e227";
+}
+
+.glyphicon-btc:before {
+  content: "\e227";
+}
+
+.glyphicon-xbt:before {
+  content: "\e227";
+}
+
+.glyphicon-yen:before {
+  content: "\00a5";
+}
+
+.glyphicon-jpy:before {
+  content: "\00a5";
+}
+
+.glyphicon-ruble:before {
+  content: "\20bd";
+}
+
+.glyphicon-rub:before {
+  content: "\20bd";
+}
+
+.glyphicon-scale:before {
+  content: "\e230";
+}
+
+.glyphicon-ice-lolly:before {
+  content: "\e231";
+}
+
+.glyphicon-ice-lolly-tasted:before {
+  content: "\e232";
+}
+
+.glyphicon-education:before {
+  content: "\e233";
+}
+
+.glyphicon-option-horizontal:before {
+  content: "\e234";
+}
+
+.glyphicon-option-vertical:before {
+  content: "\e235";
+}
+
+.glyphicon-menu-hamburger:before {
+  content: "\e236";
+}
+
+.glyphicon-modal-window:before {
+  content: "\e237";
+}
+
+.glyphicon-oil:before {
+  content: "\e238";
+}
+
+.glyphicon-grain:before {
+  content: "\e239";
+}
+
+.glyphicon-sunglasses:before {
+  content: "\e240";
+}
+
+.glyphicon-text-size:before {
+  content: "\e241";
+}
+
+.glyphicon-text-color:before {
+  content: "\e242";
+}
+
+.glyphicon-text-background:before {
+  content: "\e243";
+}
+
+.glyphicon-object-align-top:before {
+  content: "\e244";
+}
+
+.glyphicon-object-align-bottom:before {
+  content: "\e245";
+}
+
+.glyphicon-object-align-horizontal:before {
+  content: "\e246";
+}
+
+.glyphicon-object-align-left:before {
+  content: "\e247";
+}
+
+.glyphicon-object-align-vertical:before {
+  content: "\e248";
+}
+
+.glyphicon-object-align-right:before {
+  content: "\e249";
+}
+
+.glyphicon-triangle-right:before {
+  content: "\e250";
+}
+
+.glyphicon-triangle-left:before {
+  content: "\e251";
+}
+
+.glyphicon-triangle-bottom:before {
+  content: "\e252";
+}
+
+.glyphicon-triangle-top:before {
+  content: "\e253";
+}
+
+.glyphicon-console:before {
+  content: "\e254";
+}
+
+.glyphicon-superscript:before {
+  content: "\e255";
+}
+
+.glyphicon-subscript:before {
+  content: "\e256";
+}
+
+.glyphicon-menu-left:before {
+  content: "\e257";
+}
+
+.glyphicon-menu-right:before {
+  content: "\e258";
+}
+
+.glyphicon-menu-down:before {
+  content: "\e259";
+}
+
+.glyphicon-menu-up:before {
+  content: "\e260";
+}
+
+* {
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+}
+
+*:before,
+*:after {
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+}
+
+html {
+  font-size: 10px;
+  -webkit-tap-highlight-color: transparent;
+}
+
+body {
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-size: 14px;
+  line-height: 1.42857;
+  color: #333333;
+  background-color: #fff;
+}
+
+input,
+button,
+select,
+textarea {
+  font-family: inherit;
+  font-size: inherit;
+  line-height: inherit;
+}
+
+a {
+  color: #337ab7;
+  text-decoration: none;
+}
+a:hover, a:focus {
+  color: #23527c;
+  text-decoration: underline;
+}
+a:focus {
+  outline: thin dotted;
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
+
+figure {
+  margin: 0;
+}
+
+img {
+  vertical-align: middle;
+}
+
+.img-responsive {
+  display: block;
+  max-width: 100%;
+  height: auto;
+}
+
+.img-rounded {
+  border-radius: 6px;
+}
+
+.img-thumbnail {
+  padding: 4px;
+  line-height: 1.42857;
+  background-color: #fff;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  -webkit-transition: all 0.2s ease-in-out;
+  -o-transition: all 0.2s ease-in-out;
+  transition: all 0.2s ease-in-out;
+  display: inline-block;
+  max-width: 100%;
+  height: auto;
+}
+
+.img-circle {
+  border-radius: 50%;
+}
+
+hr {
+  margin-top: 20px;
+  margin-bottom: 20px;
+  border: 0;
+  border-top: 1px solid #eeeeee;
+}
+
+.sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  margin: -1px;
+  padding: 0;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  border: 0;
+}
+
+.sr-only-focusable:active, .sr-only-focusable:focus {
+  position: static;
+  width: auto;
+  height: auto;
+  margin: 0;
+  overflow: visible;
+  clip: auto;
+}
+
+[role="button"] {
+  cursor: pointer;
+}
+
+h1, h2, h3, h4, h5, h6,
+.h1, .h2, .h3, .h4, .h5, .h6 {
+  font-family: inherit;
+  font-weight: 500;
+  line-height: 1.1;
+  color: inherit;
+}
+h1 small,
+h1 .small, h2 small,
+h2 .small, h3 small,
+h3 .small, h4 small,
+h4 .small, h5 small,
+h5 .small, h6 small,
+h6 .small,
+.h1 small,
+.h1 .small, .h2 small,
+.h2 .small, .h3 small,
+.h3 .small, .h4 small,
+.h4 .small, .h5 small,
+.h5 .small, .h6 small,
+.h6 .small {
+  font-weight: normal;
+  line-height: 1;
+  color: #777777;
+}
+
+h1, .h1,
+h2, .h2,
+h3, .h3 {
+  margin-top: 20px;
+  margin-bottom: 10px;
+}
+h1 small,
+h1 .small, .h1 small,
+.h1 .small,
+h2 small,
+h2 .small, .h2 small,
+.h2 .small,
+h3 small,
+h3 .small, .h3 small,
+.h3 .small {
+  font-size: 65%;
+}
+
+h4, .h4,
+h5, .h5,
+h6, .h6 {
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+h4 small,
+h4 .small, .h4 small,
+.h4 .small,
+h5 small,
+h5 .small, .h5 small,
+.h5 .small,
+h6 small,
+h6 .small, .h6 small,
+.h6 .small {
+  font-size: 75%;
+}
+
+h1, .h1 {
+  font-size: 36px;
+}
+
+h2, .h2 {
+  font-size: 30px;
+}
+
+h3, .h3 {
+  font-size: 24px;
+}
+
+h4, .h4 {
+  font-size: 18px;
+}
+
+h5, .h5 {
+  font-size: 14px;
+}
+
+h6, .h6 {
+  font-size: 12px;
+}
+
+p {
+  margin: 0 0 10px;
+}
+
+.lead {
+  margin-bottom: 20px;
+  font-size: 16px;
+  font-weight: 300;
+  line-height: 1.4;
+}
+@media (min-width: 768px) {
+  .lead {
+    font-size: 21px;
+  }
+}
+
+small,
+.small {
+  font-size: 85%;
+}
+
+mark,
+.mark {
+  background-color: #fcf8e3;
+  padding: .2em;
+}
+
+.text-left {
+  text-align: left;
+}
+
+.text-right {
+  text-align: right;
+}
+
+.text-center {
+  text-align: center;
+}
+
+.text-justify {
+  text-align: justify;
+}
+
+.text-nowrap {
+  white-space: nowrap;
+}
+
+.text-lowercase {
+  text-transform: lowercase;
+}
+
+.text-uppercase, .initialism {
+  text-transform: uppercase;
+}
+
+.text-capitalize {
+  text-transform: capitalize;
+}
+
+.text-muted {
+  color: #777777;
+}
+
+.text-primary {
+  color: #337ab7;
+}
+
+a.text-primary:hover,
+a.text-primary:focus {
+  color: #286090;
+}
+
+.text-success {
+  color: #3c763d;
+}
+
+a.text-success:hover,
+a.text-success:focus {
+  color: #2b542c;
+}
+
+.text-info {
+  color: #31708f;
+}
+
+a.text-info:hover,
+a.text-info:focus {
+  color: #245269;
+}
+
+.text-warning {
+  color: #8a6d3b;
+}
+
+a.text-warning:hover,
+a.text-warning:focus {
+  color: #66512c;
+}
+
+.text-danger {
+  color: #a94442;
+}
+
+a.text-danger:hover,
+a.text-danger:focus {
+  color: #843534;
+}
+
+.bg-primary {
+  color: #fff;
+}
+
+.bg-primary {
+  background-color: #337ab7;
+}
+
+a.bg-primary:hover,
+a.bg-primary:focus {
+  background-color: #286090;
+}
+
+.bg-success {
+  background-color: #dff0d8;
+}
+
+a.bg-success:hover,
+a.bg-success:focus {
+  background-color: #c1e2b3;
+}
+
+.bg-info {
+  background-color: #d9edf7;
+}
+
+a.bg-info:hover,
+a.bg-info:focus {
+  background-color: #afd9ee;
+}
+
+.bg-warning {
+  background-color: #fcf8e3;
+}
+
+a.bg-warning:hover,
+a.bg-warning:focus {
+  background-color: #f7ecb5;
+}
+
+.bg-danger {
+  background-color: #f2dede;
+}
+
+a.bg-danger:hover,
+a.bg-danger:focus {
+  background-color: #e4b9b9;
+}
+
+.page-header {
+  padding-bottom: 9px;
+  margin: 40px 0 20px;
+  border-bottom: 1px solid #eeeeee;
+}
+
+ul,
+ol {
+  margin-top: 0;
+  margin-bottom: 10px;
+}
+ul ul,
+ul ol,
+ol ul,
+ol ol {
+  margin-bottom: 0;
+}
+
+.list-unstyled {
+  padding-left: 0;
+  list-style: none;
+}
+
+.list-inline {
+  padding-left: 0;
+  list-style: none;
+  margin-left: -5px;
+}
+.list-inline > li {
+  display: inline-block;
+  padding-left: 5px;
+  padding-right: 5px;
+}
+
+dl {
+  margin-top: 0;
+  margin-bottom: 20px;
+}
+
+dt,
+dd {
+  line-height: 1.42857;
+}
+
+dt {
+  font-weight: bold;
+}
+
+dd {
+  margin-left: 0;
+}
+
+.dl-horizontal dd:before, .dl-horizontal dd:after {
+  content: " ";
+  display: table;
+}
+.dl-horizontal dd:after {
+  clear: both;
+}
+@media (min-width: 768px) {
+  .dl-horizontal dt {
+    float: left;
+    width: 160px;
+    clear: left;
+    text-align: right;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+  .dl-horizontal dd {
+    margin-left: 180px;
+  }
+}
+
+abbr[title],
+abbr[data-original-title] {
+  cursor: help;
+  border-bottom: 1px dotted #777777;
+}
+
+.initialism {
+  font-size: 90%;
+}
+
+blockquote {
+  padding: 10px 20px;
+  margin: 0 0 20px;
+  font-size: 17.5px;
+  border-left: 5px solid #eeeeee;
+}
+blockquote p:last-child,
+blockquote ul:last-child,
+blockquote ol:last-child {
+  margin-bottom: 0;
+}
+blockquote footer,
+blockquote small,
+blockquote .small {
+  display: block;
+  font-size: 80%;
+  line-height: 1.42857;
+  color: #777777;
+}
+blockquote footer:before,
+blockquote small:before,
+blockquote .small:before {
+  content: '\2014 \00A0';
+}
+
+.blockquote-reverse,
+blockquote.pull-right {
+  padding-right: 15px;
+  padding-left: 0;
+  border-right: 5px solid #eeeeee;
+  border-left: 0;
+  text-align: right;
+}
+.blockquote-reverse footer:before,
+.blockquote-reverse small:before,
+.blockquote-reverse .small:before,
+blockquote.pull-right footer:before,
+blockquote.pull-right small:before,
+blockquote.pull-right .small:before {
+  content: '';
+}
+.blockquote-reverse footer:after,
+.blockquote-reverse small:after,
+.blockquote-reverse .small:after,
+blockquote.pull-right footer:after,
+blockquote.pull-right small:after,
+blockquote.pull-right .small:after {
+  content: '\00A0 \2014';
+}
+
+address {
+  margin-bottom: 20px;
+  font-style: normal;
+  line-height: 1.42857;
+}
+
+code,
+kbd,
+pre,
+samp {
+  font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
+}
+
+code {
+  padding: 2px 4px;
+  font-size: 90%;
+  color: #c7254e;
+  background-color: #f9f2f4;
+  border-radius: 4px;
+}
+
+kbd {
+  padding: 2px 4px;
+  font-size: 90%;
+  color: #fff;
+  background-color: #333;
+  border-radius: 3px;
+  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+kbd kbd {
+  padding: 0;
+  font-size: 100%;
+  font-weight: bold;
+  box-shadow: none;
+}
+
+pre {
+  display: block;
+  padding: 9.5px;
+  margin: 0 0 10px;
+  font-size: 13px;
+  line-height: 1.42857;
+  word-break: break-all;
+  word-wrap: break-word;
+  color: #333333;
+  background-color: #f5f5f5;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+}
+pre code {
+  padding: 0;
+  font-size: inherit;
+  color: inherit;
+  white-space: pre-wrap;
+  background-color: transparent;
+  border-radius: 0;
+}
+
+.pre-scrollable {
+  max-height: 340px;
+  overflow-y: scroll;
+}
+
+.container {
+  margin-right: auto;
+  margin-left: auto;
+  padding-left: 15px;
+  padding-right: 15px;
+}
+.container:before, .container:after {
+  content: " ";
+  display: table;
+}
+.container:after {
+  clear: both;
+}
+@media (min-width: 768px) {
+  .container {
+    width: 750px;
+  }
+}
+@media (min-width: 992px) {
+  .container {
+    width: 970px;
+  }
+}
+@media (min-width: 1200px) {
+  .container {
+    width: 1170px;
+  }
+}
+
+.container-fluid {
+  margin-right: auto;
+  margin-left: auto;
+  padding-left: 15px;
+  padding-right: 15px;
+}
+.container-fluid:before, .container-fluid:after {
+  content: " ";
+  display: table;
+}
+.container-fluid:after {
+  clear: both;
+}
+
+.row {
+  margin-left: -15px;
+  margin-right: -15px;
+}
+.row:before, .row:after {
+  content: " ";
+  display: table;
+}
+.row:after {
+  clear: both;
+}
+
+.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {
+  position: relative;
+  min-height: 1px;
+  padding-left: 15px;
+  padding-right: 15px;
+}
+
+.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {
+  float: left;
+}
+
+.col-xs-1 {
+  width: 8.33333%;
+}
+
+.col-xs-2 {
+  width: 16.66667%;
+}
+
+.col-xs-3 {
+  width: 25%;
+}
+
+.col-xs-4 {
+  width: 33.33333%;
+}
+
+.col-xs-5 {
+  width: 41.66667%;
+}
+
+.col-xs-6 {
+  width: 50%;
+}
+
+.col-xs-7 {
+  width: 58.33333%;
+}
+
+.col-xs-8 {
+  width: 66.66667%;
+}
+
+.col-xs-9 {
+  width: 75%;
+}
+
+.col-xs-10 {
+  width: 83.33333%;
+}
+
+.col-xs-11 {
+  width: 91.66667%;
+}
+
+.col-xs-12 {
+  width: 100%;
+}
+
+.col-xs-pull-0 {
+  right: auto;
+}
+
+.col-xs-pull-1 {
+  right: 8.33333%;
+}
+
+.col-xs-pull-2 {
+  right: 16.66667%;
+}
+
+.col-xs-pull-3 {
+  right: 25%;
+}
+
+.col-xs-pull-4 {
+  right: 33.33333%;
+}
+
+.col-xs-pull-5 {
+  right: 41.66667%;
+}
+
+.col-xs-pull-6 {
+  right: 50%;
+}
+
+.col-xs-pull-7 {
+  right: 58.33333%;
+}
+
+.col-xs-pull-8 {
+  right: 66.66667%;
+}
+
+.col-xs-pull-9 {
+  right: 75%;
+}
+
+.col-xs-pull-10 {
+  right: 83.33333%;
+}
+
+.col-xs-pull-11 {
+  right: 91.66667%;
+}
+
+.col-xs-pull-12 {
+  right: 100%;
+}
+
+.col-xs-push-0 {
+  left: auto;
+}
+
+.col-xs-push-1 {
+  left: 8.33333%;
+}
+
+.col-xs-push-2 {
+  left: 16.66667%;
+}
+
+.col-xs-push-3 {
+  left: 25%;
+}
+
+.col-xs-push-4 {
+  left: 33.33333%;
+}
+
+.col-xs-push-5 {
+  left: 41.66667%;
+}
+
+.col-xs-push-6 {
+  left: 50%;
+}
+
+.col-xs-push-7 {
+  left: 58.33333%;
+}
+
+.col-xs-push-8 {
+  left: 66.66667%;
+}
+
+.col-xs-push-9 {
+  left: 75%;
+}
+
+.col-xs-push-10 {
+  left: 83.33333%;
+}
+
+.col-xs-push-11 {
+  left: 91.66667%;
+}
+
+.col-xs-push-12 {
+  left: 100%;
+}
+
+.col-xs-offset-0 {
+  margin-left: 0%;
+}
+
+.col-xs-offset-1 {
+  margin-left: 8.33333%;
+}
+
+.col-xs-offset-2 {
+  margin-left: 16.66667%;
+}
+
+.col-xs-offset-3 {
+  margin-left: 25%;
+}
+
+.col-xs-offset-4 {
+  margin-left: 33.33333%;
+}
+
+.col-xs-offset-5 {
+  margin-left: 41.66667%;
+}
+
+.col-xs-offset-6 {
+  margin-left: 50%;
+}
+
+.col-xs-offset-7 {
+  margin-left: 58.33333%;
+}
+
+.col-xs-offset-8 {
+  margin-left: 66.66667%;
+}
+
+.col-xs-offset-9 {
+  margin-left: 75%;
+}
+
+.col-xs-offset-10 {
+  margin-left: 83.33333%;
+}
+
+.col-xs-offset-11 {
+  margin-left: 91.66667%;
+}
+
+.col-xs-offset-12 {
+  margin-left: 100%;
+}
+
+@media (min-width: 768px) {
+  .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {
+    float: left;
+  }
+
+  .col-sm-1 {
+    width: 8.33333%;
+  }
+
+  .col-sm-2 {
+    width: 16.66667%;
+  }
+
+  .col-sm-3 {
+    width: 25%;
+  }
+
+  .col-sm-4 {
+    width: 33.33333%;
+  }
+
+  .col-sm-5 {
+    width: 41.66667%;
+  }
+
+  .col-sm-6 {
+    width: 50%;
+  }
+
+  .col-sm-7 {
+    width: 58.33333%;
+  }
+
+  .col-sm-8 {
+    width: 66.66667%;
+  }
+
+  .col-sm-9 {
+    width: 75%;
+  }
+
+  .col-sm-10 {
+    width: 83.33333%;
+  }
+
+  .col-sm-11 {
+    width: 91.66667%;
+  }
+
+  .col-sm-12 {
+    width: 100%;
+  }
+
+  .col-sm-pull-0 {
+    right: auto;
+  }
+
+  .col-sm-pull-1 {
+    right: 8.33333%;
+  }
+
+  .col-sm-pull-2 {
+    right: 16.66667%;
+  }
+
+  .col-sm-pull-3 {
+    right: 25%;
+  }
+
+  .col-sm-pull-4 {
+    right: 33.33333%;
+  }
+
+  .col-sm-pull-5 {
+    right: 41.66667%;
+  }
+
+  .col-sm-pull-6 {
+    right: 50%;
+  }
+
+  .col-sm-pull-7 {
+    right: 58.33333%;
+  }
+
+  .col-sm-pull-8 {
+    right: 66.66667%;
+  }
+
+  .col-sm-pull-9 {
+    right: 75%;
+  }
+
+  .col-sm-pull-10 {
+    right: 83.33333%;
+  }
+
+  .col-sm-pull-11 {
+    right: 91.66667%;
+  }
+
+  .col-sm-pull-12 {
+    right: 100%;
+  }
+
+  .col-sm-push-0 {
+    left: auto;
+  }
+
+  .col-sm-push-1 {
+    left: 8.33333%;
+  }
+
+  .col-sm-push-2 {
+    left: 16.66667%;
+  }
+
+  .col-sm-push-3 {
+    left: 25%;
+  }
+
+  .col-sm-push-4 {
+    left: 33.33333%;
+  }
+
+  .col-sm-push-5 {
+    left: 41.66667%;
+  }
+
+  .col-sm-push-6 {
+    left: 50%;
+  }
+
+  .col-sm-push-7 {
+    left: 58.33333%;
+  }
+
+  .col-sm-push-8 {
+    left: 66.66667%;
+  }
+
+  .col-sm-push-9 {
+    left: 75%;
+  }
+
+  .col-sm-push-10 {
+    left: 83.33333%;
+  }
+
+  .col-sm-push-11 {
+    left: 91.66667%;
+  }
+
+  .col-sm-push-12 {
+    left: 100%;
+  }
+
+  .col-sm-offset-0 {
+    margin-left: 0%;
+  }
+
+  .col-sm-offset-1 {
+    margin-left: 8.33333%;
+  }
+
+  .col-sm-offset-2 {
+    margin-left: 16.66667%;
+  }
+
+  .col-sm-offset-3 {
+    margin-left: 25%;
+  }
+
+  .col-sm-offset-4 {
+    margin-left: 33.33333%;
+  }
+
+  .col-sm-offset-5 {
+    margin-left: 41.66667%;
+  }
+
+  .col-sm-offset-6 {
+    margin-left: 50%;
+  }
+
+  .col-sm-offset-7 {
+    margin-left: 58.33333%;
+  }
+
+  .col-sm-offset-8 {
+    margin-left: 66.66667%;
+  }
+
+  .col-sm-offset-9 {
+    margin-left: 75%;
+  }
+
+  .col-sm-offset-10 {
+    margin-left: 83.33333%;
+  }
+
+  .col-sm-offset-11 {
+    margin-left: 91.66667%;
+  }
+
+  .col-sm-offset-12 {
+    margin-left: 100%;
+  }
+}
+@media (min-width: 992px) {
+  .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {
+    float: left;
+  }
+
+  .col-md-1 {
+    width: 8.33333%;
+  }
+
+  .col-md-2 {
+    width: 16.66667%;
+  }
+
+  .col-md-3 {
+    width: 25%;
+  }
+
+  .col-md-4 {
+    width: 33.33333%;
+  }
+
+  .col-md-5 {
+    width: 41.66667%;
+  }
+
+  .col-md-6 {
+    width: 50%;
+  }
+
+  .col-md-7 {
+    width: 58.33333%;
+  }
+
+  .col-md-8 {
+    width: 66.66667%;
+  }
+
+  .col-md-9 {
+    width: 75%;
+  }
+
+  .col-md-10 {
+    width: 83.33333%;
+  }
+
+  .col-md-11 {
+    width: 91.66667%;
+  }
+
+  .col-md-12 {
+    width: 100%;
+  }
+
+  .col-md-pull-0 {
+    right: auto;
+  }
+
+  .col-md-pull-1 {
+    right: 8.33333%;
+  }
+
+  .col-md-pull-2 {
+    right: 16.66667%;
+  }
+
+  .col-md-pull-3 {
+    right: 25%;
+  }
+
+  .col-md-pull-4 {
+    right: 33.33333%;
+  }
+
+  .col-md-pull-5 {
+    right: 41.66667%;
+  }
+
+  .col-md-pull-6 {
+    right: 50%;
+  }
+
+  .col-md-pull-7 {
+    right: 58.33333%;
+  }
+
+  .col-md-pull-8 {
+    right: 66.66667%;
+  }
+
+  .col-md-pull-9 {
+    right: 75%;
+  }
+
+  .col-md-pull-10 {
+    right: 83.33333%;
+  }
+
+  .col-md-pull-11 {
+    right: 91.66667%;
+  }
+
+  .col-md-pull-12 {
+    right: 100%;
+  }
+
+  .col-md-push-0 {
+    left: auto;
+  }
+
+  .col-md-push-1 {
+    left: 8.33333%;
+  }
+
+  .col-md-push-2 {
+    left: 16.66667%;
+  }
+
+  .col-md-push-3 {
+    left: 25%;
+  }
+
+  .col-md-push-4 {
+    left: 33.33333%;
+  }
+
+  .col-md-push-5 {
+    left: 41.66667%;
+  }
+
+  .col-md-push-6 {
+    left: 50%;
+  }
+
+  .col-md-push-7 {
+    left: 58.33333%;
+  }
+
+  .col-md-push-8 {
+    left: 66.66667%;
+  }
+
+  .col-md-push-9 {
+    left: 75%;
+  }
+
+  .col-md-push-10 {
+    left: 83.33333%;
+  }
+
+  .col-md-push-11 {
+    left: 91.66667%;
+  }
+
+  .col-md-push-12 {
+    left: 100%;
+  }
+
+  .col-md-offset-0 {
+    margin-left: 0%;
+  }
+
+  .col-md-offset-1 {
+    margin-left: 8.33333%;
+  }
+
+  .col-md-offset-2 {
+    margin-left: 16.66667%;
+  }
+
+  .col-md-offset-3 {
+    margin-left: 25%;
+  }
+
+  .col-md-offset-4 {
+    margin-left: 33.33333%;
+  }
+
+  .col-md-offset-5 {
+    margin-left: 41.66667%;
+  }
+
+  .col-md-offset-6 {
+    margin-left: 50%;
+  }
+
+  .col-md-offset-7 {
+    margin-left: 58.33333%;
+  }
+
+  .col-md-offset-8 {
+    margin-left: 66.66667%;
+  }
+
+  .col-md-offset-9 {
+    margin-left: 75%;
+  }
+
+  .col-md-offset-10 {
+    margin-left: 83.33333%;
+  }
+
+  .col-md-offset-11 {
+    margin-left: 91.66667%;
+  }
+
+  .col-md-offset-12 {
+    margin-left: 100%;
+  }
+}
+@media (min-width: 1200px) {
+  .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {
+    float: left;
+  }
+
+  .col-lg-1 {
+    width: 8.33333%;
+  }
+
+  .col-lg-2 {
+    width: 16.66667%;
+  }
+
+  .col-lg-3 {
+    width: 25%;
+  }
+
+  .col-lg-4 {
+    width: 33.33333%;
+  }
+
+  .col-lg-5 {
+    width: 41.66667%;
+  }
+
+  .col-lg-6 {
+    width: 50%;
+  }
+
+  .col-lg-7 {
+    width: 58.33333%;
+  }
+
+  .col-lg-8 {
+    width: 66.66667%;
+  }
+
+  .col-lg-9 {
+    width: 75%;
+  }
+
+  .col-lg-10 {
+    width: 83.33333%;
+  }
+
+  .col-lg-11 {
+    width: 91.66667%;
+  }
+
+  .col-lg-12 {
+    width: 100%;
+  }
+
+  .col-lg-pull-0 {
+    right: auto;
+  }
+
+  .col-lg-pull-1 {
+    right: 8.33333%;
+  }
+
+  .col-lg-pull-2 {
+    right: 16.66667%;
+  }
+
+  .col-lg-pull-3 {
+    right: 25%;
+  }
+
+  .col-lg-pull-4 {
+    right: 33.33333%;
+  }
+
+  .col-lg-pull-5 {
+    right: 41.66667%;
+  }
+
+  .col-lg-pull-6 {
+    right: 50%;
+  }
+
+  .col-lg-pull-7 {
+    right: 58.33333%;
+  }
+
+  .col-lg-pull-8 {
+    right: 66.66667%;
+  }
+
+  .col-lg-pull-9 {
+    right: 75%;
+  }
+
+  .col-lg-pull-10 {
+    right: 83.33333%;
+  }
+
+  .col-lg-pull-11 {
+    right: 91.66667%;
+  }
+
+  .col-lg-pull-12 {
+    right: 100%;
+  }
+
+  .col-lg-push-0 {
+    left: auto;
+  }
+
+  .col-lg-push-1 {
+    left: 8.33333%;
+  }
+
+  .col-lg-push-2 {
+    left: 16.66667%;
+  }
+
+  .col-lg-push-3 {
+    left: 25%;
+  }
+
+  .col-lg-push-4 {
+    left: 33.33333%;
+  }
+
+  .col-lg-push-5 {
+    left: 41.66667%;
+  }
+
+  .col-lg-push-6 {
+    left: 50%;
+  }
+
+  .col-lg-push-7 {
+    left: 58.33333%;
+  }
+
+  .col-lg-push-8 {
+    left: 66.66667%;
+  }
+
+  .col-lg-push-9 {
+    left: 75%;
+  }
+
+  .col-lg-push-10 {
+    left: 83.33333%;
+  }
+
+  .col-lg-push-11 {
+    left: 91.66667%;
+  }
+
+  .col-lg-push-12 {
+    left: 100%;
+  }
+
+  .col-lg-offset-0 {
+    margin-left: 0%;
+  }
+
+  .col-lg-offset-1 {
+    margin-left: 8.33333%;
+  }
+
+  .col-lg-offset-2 {
+    margin-left: 16.66667%;
+  }
+
+  .col-lg-offset-3 {
+    margin-left: 25%;
+  }
+
+  .col-lg-offset-4 {
+    margin-left: 33.33333%;
+  }
+
+  .col-lg-offset-5 {
+    margin-left: 41.66667%;
+  }
+
+  .col-lg-offset-6 {
+    margin-left: 50%;
+  }
+
+  .col-lg-offset-7 {
+    margin-left: 58.33333%;
+  }
+
+  .col-lg-offset-8 {
+    margin-left: 66.66667%;
+  }
+
+  .col-lg-offset-9 {
+    margin-left: 75%;
+  }
+
+  .col-lg-offset-10 {
+    margin-left: 83.33333%;
+  }
+
+  .col-lg-offset-11 {
+    margin-left: 91.66667%;
+  }
+
+  .col-lg-offset-12 {
+    margin-left: 100%;
+  }
+}
+table {
+  background-color: transparent;
+}
+
+caption {
+  padding-top: 8px;
+  padding-bottom: 8px;
+  color: #777777;
+  text-align: left;
+}
+
+th {
+  text-align: left;
+}
+
+.table {
+  width: 100%;
+  max-width: 100%;
+  margin-bottom: 20px;
+}
+.table > thead > tr > th,
+.table > thead > tr > td,
+.table > tbody > tr > th,
+.table > tbody > tr > td,
+.table > tfoot > tr > th,
+.table > tfoot > tr > td {
+  padding: 8px;
+  line-height: 1.42857;
+  vertical-align: top;
+  border-top: 1px solid #ddd;
+}
+.table > thead > tr > th {
+  vertical-align: bottom;
+  border-bottom: 2px solid #ddd;
+}
+.table > caption + thead > tr:first-child > th,
+.table > caption + thead > tr:first-child > td,
+.table > colgroup + thead > tr:first-child > th,
+.table > colgroup + thead > tr:first-child > td,
+.table > thead:first-child > tr:first-child > th,
+.table > thead:first-child > tr:first-child > td {
+  border-top: 0;
+}
+.table > tbody + tbody {
+  border-top: 2px solid #ddd;
+}
+.table .table {
+  background-color: #fff;
+}
+
+.table-condensed > thead > tr > th,
+.table-condensed > thead > tr > td,
+.table-condensed > tbody > tr > th,
+.table-condensed > tbody > tr > td,
+.table-condensed > tfoot > tr > th,
+.table-condensed > tfoot > tr > td {
+  padding: 5px;
+}
+
+.table-bordered {
+  border: 1px solid #ddd;
+}
+.table-bordered > thead > tr > th,
+.table-bordered > thead > tr > td,
+.table-bordered > tbody > tr > th,
+.table-bordered > tbody > tr > td,
+.table-bordered > tfoot > tr > th,
+.table-bordered > tfoot > tr > td {
+  border: 1px solid #ddd;
+}
+.table-bordered > thead > tr > th,
+.table-bordered > thead > tr > td {
+  border-bottom-width: 2px;
+}
+
+.table-striped > tbody > tr:nth-of-type(odd) {
+  background-color: #f9f9f9;
+}
+
+.table-hover > tbody > tr:hover {
+  background-color: #f5f5f5;
+}
+
+table col[class*="col-"] {
+  position: static;
+  float: none;
+  display: table-column;
+}
+
+table td[class*="col-"],
+table th[class*="col-"] {
+  position: static;
+  float: none;
+  display: table-cell;
+}
+
+.table > thead > tr > td.active,
+.table > thead > tr > th.active, .table > thead > tr.active > td, .table > thead > tr.active > th,
+.table > tbody > tr > td.active,
+.table > tbody > tr > th.active,
+.table > tbody > tr.active > td,
+.table > tbody > tr.active > th,
+.table > tfoot > tr > td.active,
+.table > tfoot > tr > th.active,
+.table > tfoot > tr.active > td,
+.table > tfoot > tr.active > th {
+  background-color: #f5f5f5;
+}
+
+.table-hover > tbody > tr > td.active:hover,
+.table-hover > tbody > tr > th.active:hover, .table-hover > tbody > tr.active:hover > td, .table-hover > tbody > tr:hover > .active, .table-hover > tbody > tr.active:hover > th {
+  background-color: #e8e8e8;
+}
+
+.table > thead > tr > td.success,
+.table > thead > tr > th.success, .table > thead > tr.success > td, .table > thead > tr.success > th,
+.table > tbody > tr > td.success,
+.table > tbody > tr > th.success,
+.table > tbody > tr.success > td,
+.table > tbody > tr.success > th,
+.table > tfoot > tr > td.success,
+.table > tfoot > tr > th.success,
+.table > tfoot > tr.success > td,
+.table > tfoot > tr.success > th {
+  background-color: #dff0d8;
+}
+
+.table-hover > tbody > tr > td.success:hover,
+.table-hover > tbody > tr > th.success:hover, .table-hover > tbody > tr.success:hover > td, .table-hover > tbody > tr:hover > .success, .table-hover > tbody > tr.success:hover > th {
+  background-color: #d0e9c6;
+}
+
+.table > thead > tr > td.info,
+.table > thead > tr > th.info, .table > thead > tr.info > td, .table > thead > tr.info > th,
+.table > tbody > tr > td.info,
+.table > tbody > tr > th.info,
+.table > tbody > tr.info > td,
+.table > tbody > tr.info > th,
+.table > tfoot > tr > td.info,
+.table > tfoot > tr > th.info,
+.table > tfoot > tr.info > td,
+.table > tfoot > tr.info > th {
+  background-color: #d9edf7;
+}
+
+.table-hover > tbody > tr > td.info:hover,
+.table-hover > tbody > tr > th.info:hover, .table-hover > tbody > tr.info:hover > td, .table-hover > tbody > tr:hover > .info, .table-hover > tbody > tr.info:hover > th {
+  background-color: #c4e3f3;
+}
+
+.table > thead > tr > td.warning,
+.table > thead > tr > th.warning, .table > thead > tr.warning > td, .table > thead > tr.warning > th,
+.table > tbody > tr > td.warning,
+.table > tbody > tr > th.warning,
+.table > tbody > tr.warning > td,
+.table > tbody > tr.warning > th,
+.table > tfoot > tr > td.warning,
+.table > tfoot > tr > th.warning,
+.table > tfoot > tr.warning > td,
+.table > tfoot > tr.warning > th {
+  background-color: #fcf8e3;
+}
+
+.table-hover > tbody > tr > td.warning:hover,
+.table-hover > tbody > tr > th.warning:hover, .table-hover > tbody > tr.warning:hover > td, .table-hover > tbody > tr:hover > .warning, .table-hover > tbody > tr.warning:hover > th {
+  background-color: #faf2cc;
+}
+
+.table > thead > tr > td.danger,
+.table > thead > tr > th.danger, .table > thead > tr.danger > td, .table > thead > tr.danger > th,
+.table > tbody > tr > td.danger,
+.table > tbody > tr > th.danger,
+.table > tbody > tr.danger > td,
+.table > tbody > tr.danger > th,
+.table > tfoot > tr > td.danger,
+.table > tfoot > tr > th.danger,
+.table > tfoot > tr.danger > td,
+.table > tfoot > tr.danger > th {
+  background-color: #f2dede;
+}
+
+.table-hover > tbody > tr > td.danger:hover,
+.table-hover > tbody > tr > th.danger:hover, .table-hover > tbody > tr.danger:hover > td, .table-hover > tbody > tr:hover > .danger, .table-hover > tbody > tr.danger:hover > th {
+  background-color: #ebcccc;
+}
+
+.table-responsive {
+  overflow-x: auto;
+  min-height: 0.01%;
+}
+@media screen and (max-width: 767px) {
+  .table-responsive {
+    width: 100%;
+    margin-bottom: 15px;
+    overflow-y: hidden;
+    -ms-overflow-style: -ms-autohiding-scrollbar;
+    border: 1px solid #ddd;
+  }
+  .table-responsive > .table {
+    margin-bottom: 0;
+  }
+  .table-responsive > .table > thead > tr > th,
+  .table-responsive > .table > thead > tr > td,
+  .table-responsive > .table > tbody > tr > th,
+  .table-responsive > .table > tbody > tr > td,
+  .table-responsive > .table > tfoot > tr > th,
+  .table-responsive > .table > tfoot > tr > td {
+    white-space: nowrap;
+  }
+  .table-responsive > .table-bordered {
+    border: 0;
+  }
+  .table-responsive > .table-bordered > thead > tr > th:first-child,
+  .table-responsive > .table-bordered > thead > tr > td:first-child,
+  .table-responsive > .table-bordered > tbody > tr > th:first-child,
+  .table-responsive > .table-bordered > tbody > tr > td:first-child,
+  .table-responsive > .table-bordered > tfoot > tr > th:first-child,
+  .table-responsive > .table-bordered > tfoot > tr > td:first-child {
+    border-left: 0;
+  }
+  .table-responsive > .table-bordered > thead > tr > th:last-child,
+  .table-responsive > .table-bordered > thead > tr > td:last-child,
+  .table-responsive > .table-bordered > tbody > tr > th:last-child,
+  .table-responsive > .table-bordered > tbody > tr > td:last-child,
+  .table-responsive > .table-bordered > tfoot > tr > th:last-child,
+  .table-responsive > .table-bordered > tfoot > tr > td:last-child {
+    border-right: 0;
+  }
+  .table-responsive > .table-bordered > tbody > tr:last-child > th,
+  .table-responsive > .table-bordered > tbody > tr:last-child > td,
+  .table-responsive > .table-bordered > tfoot > tr:last-child > th,
+  .table-responsive > .table-bordered > tfoot > tr:last-child > td {
+    border-bottom: 0;
+  }
+}
+
+fieldset {
+  padding: 0;
+  margin: 0;
+  border: 0;
+  min-width: 0;
+}
+
+legend {
+  display: block;
+  width: 100%;
+  padding: 0;
+  margin-bottom: 20px;
+  font-size: 21px;
+  line-height: inherit;
+  color: #333333;
+  border: 0;
+  border-bottom: 1px solid #e5e5e5;
+}
+
+label {
+  display: inline-block;
+  max-width: 100%;
+  margin-bottom: 5px;
+  font-weight: bold;
+}
+
+input[type="search"] {
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+}
+
+input[type="radio"],
+input[type="checkbox"] {
+  margin: 4px 0 0;
+  margin-top: 1px \9;
+  line-height: normal;
+}
+
+input[type="file"] {
+  display: block;
+}
+
+input[type="range"] {
+  display: block;
+  width: 100%;
+}
+
+select[multiple],
+select[size] {
+  height: auto;
+}
+
+input[type="file"]:focus,
+input[type="radio"]:focus,
+input[type="checkbox"]:focus {
+  outline: thin dotted;
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
+
+output {
+  display: block;
+  padding-top: 11px;
+  font-size: 14px;
+  line-height: 1.42857;
+  color: #555555;
+}
+
+.form-control {
+  display: block;
+  width: 100%;
+  height: 42px;
+  padding: 10px 20px;
+  font-size: 14px;
+  line-height: 1.42857;
+  color: #555555;
+  background-color: #fff;
+  background-image: none;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+  -o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+  transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+}
+.form-control:focus {
+  border-color: #66afe9;
+  outline: 0;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+}
+.form-control::-moz-placeholder {
+  color: #999;
+  opacity: 1;
+}
+.form-control:-ms-input-placeholder {
+  color: #999;
+}
+.form-control::-webkit-input-placeholder {
+  color: #999;
+}
+.form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control {
+  background-color: #eeeeee;
+  opacity: 1;
+}
+.form-control[disabled], fieldset[disabled] .form-control {
+  cursor: not-allowed;
+}
+
+textarea.form-control {
+  height: auto;
+}
+
+input[type="search"] {
+  -webkit-appearance: none;
+}
+
+@media screen and (-webkit-min-device-pixel-ratio: 0) {
+  input[type="date"].form-control,
+  input[type="time"].form-control,
+  input[type="datetime-local"].form-control,
+  input[type="month"].form-control {
+    line-height: 42px;
+  }
+  input[type="date"].input-sm, .input-group-sm > input[type="date"].form-control,
+  .input-group-sm > input[type="date"].input-group-addon,
+  .input-group-sm > .input-group-btn > input[type="date"].btn, .input-group-sm input[type="date"],
+  input[type="time"].input-sm,
+  .input-group-sm > input[type="time"].form-control,
+  .input-group-sm > input[type="time"].input-group-addon,
+  .input-group-sm > .input-group-btn > input[type="time"].btn, .input-group-sm
+  input[type="time"],
+  input[type="datetime-local"].input-sm,
+  .input-group-sm > input[type="datetime-local"].form-control,
+  .input-group-sm > input[type="datetime-local"].input-group-addon,
+  .input-group-sm > .input-group-btn > input[type="datetime-local"].btn, .input-group-sm
+  input[type="datetime-local"],
+  input[type="month"].input-sm,
+  .input-group-sm > input[type="month"].form-control,
+  .input-group-sm > input[type="month"].input-group-addon,
+  .input-group-sm > .input-group-btn > input[type="month"].btn, .input-group-sm
+  input[type="month"] {
+    line-height: 30px;
+  }
+  input[type="date"].input-lg, .input-group-lg > input[type="date"].form-control,
+  .input-group-lg > input[type="date"].input-group-addon,
+  .input-group-lg > .input-group-btn > input[type="date"].btn, .input-group-lg input[type="date"],
+  input[type="time"].input-lg,
+  .input-group-lg > input[type="time"].form-control,
+  .input-group-lg > input[type="time"].input-group-addon,
+  .input-group-lg > .input-group-btn > input[type="time"].btn, .input-group-lg
+  input[type="time"],
+  input[type="datetime-local"].input-lg,
+  .input-group-lg > input[type="datetime-local"].form-control,
+  .input-group-lg > input[type="datetime-local"].input-group-addon,
+  .input-group-lg > .input-group-btn > input[type="datetime-local"].btn, .input-group-lg
+  input[type="datetime-local"],
+  input[type="month"].input-lg,
+  .input-group-lg > input[type="month"].form-control,
+  .input-group-lg > input[type="month"].input-group-addon,
+  .input-group-lg > .input-group-btn > input[type="month"].btn, .input-group-lg
+  input[type="month"] {
+    line-height: 46px;
+  }
+}
+.form-group {
+  margin-bottom: 15px;
+}
+
+.radio,
+.checkbox {
+  position: relative;
+  display: block;
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+.radio label,
+.checkbox label {
+  min-height: 20px;
+  padding-left: 20px;
+  margin-bottom: 0;
+  font-weight: normal;
+  cursor: pointer;
+}
+
+.radio input[type="radio"],
+.radio-inline input[type="radio"],
+.checkbox input[type="checkbox"],
+.checkbox-inline input[type="checkbox"] {
+  position: absolute;
+  margin-left: -20px;
+  margin-top: 4px \9;
+}
+
+.radio + .radio,
+.checkbox + .checkbox {
+  margin-top: -5px;
+}
+
+.radio-inline,
+.checkbox-inline {
+  position: relative;
+  display: inline-block;
+  padding-left: 20px;
+  margin-bottom: 0;
+  vertical-align: middle;
+  font-weight: normal;
+  cursor: pointer;
+}
+
+.radio-inline + .radio-inline,
+.checkbox-inline + .checkbox-inline {
+  margin-top: 0;
+  margin-left: 10px;
+}
+
+input[type="radio"][disabled], input[type="radio"].disabled, fieldset[disabled] input[type="radio"],
+input[type="checkbox"][disabled],
+input[type="checkbox"].disabled, fieldset[disabled]
+input[type="checkbox"] {
+  cursor: not-allowed;
+}
+
+.radio-inline.disabled, fieldset[disabled] .radio-inline,
+.checkbox-inline.disabled, fieldset[disabled]
+.checkbox-inline {
+  cursor: not-allowed;
+}
+
+.radio.disabled label, fieldset[disabled] .radio label,
+.checkbox.disabled label, fieldset[disabled]
+.checkbox label {
+  cursor: not-allowed;
+}
+
+.form-control-static {
+  padding-top: 11px;
+  padding-bottom: 11px;
+  margin-bottom: 0;
+  min-height: 34px;
+}
+.form-control-static.input-lg, .input-group-lg > .form-control-static.form-control,
+.input-group-lg > .form-control-static.input-group-addon,
+.input-group-lg > .input-group-btn > .form-control-static.btn, .form-control-static.input-sm, .input-group-sm > .form-control-static.form-control,
+.input-group-sm > .form-control-static.input-group-addon,
+.input-group-sm > .input-group-btn > .form-control-static.btn {
+  padding-left: 0;
+  padding-right: 0;
+}
+
+.input-sm, .input-group-sm > .form-control,
+.input-group-sm > .input-group-addon,
+.input-group-sm > .input-group-btn > .btn {
+  height: 30px;
+  padding: 5px 20px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+
+select.input-sm, .input-group-sm > select.form-control,
+.input-group-sm > select.input-group-addon,
+.input-group-sm > .input-group-btn > select.btn {
+  height: 30px;
+  line-height: 30px;
+}
+
+textarea.input-sm, .input-group-sm > textarea.form-control,
+.input-group-sm > textarea.input-group-addon,
+.input-group-sm > .input-group-btn > textarea.btn,
+select[multiple].input-sm,
+.input-group-sm > select[multiple].form-control,
+.input-group-sm > select[multiple].input-group-addon,
+.input-group-sm > .input-group-btn > select[multiple].btn {
+  height: auto;
+}
+
+.form-group-sm .form-control {
+  height: 30px;
+  padding: 5px 20px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+.form-group-sm select.form-control {
+  height: 30px;
+  line-height: 30px;
+}
+.form-group-sm textarea.form-control,
+.form-group-sm select[multiple].form-control {
+  height: auto;
+}
+.form-group-sm .form-control-static {
+  height: 30px;
+  min-height: 32px;
+  padding: 6px 20px;
+  font-size: 12px;
+  line-height: 1.5;
+}
+
+.input-lg, .input-group-lg > .form-control,
+.input-group-lg > .input-group-addon,
+.input-group-lg > .input-group-btn > .btn {
+  height: 46px;
+  padding: 10px 20px;
+  font-size: 18px;
+  line-height: 1.33333;
+  border-radius: 6px;
+}
+
+select.input-lg, .input-group-lg > select.form-control,
+.input-group-lg > select.input-group-addon,
+.input-group-lg > .input-group-btn > select.btn {
+  height: 46px;
+  line-height: 46px;
+}
+
+textarea.input-lg, .input-group-lg > textarea.form-control,
+.input-group-lg > textarea.input-group-addon,
+.input-group-lg > .input-group-btn > textarea.btn,
+select[multiple].input-lg,
+.input-group-lg > select[multiple].form-control,
+.input-group-lg > select[multiple].input-group-addon,
+.input-group-lg > .input-group-btn > select[multiple].btn {
+  height: auto;
+}
+
+.form-group-lg .form-control {
+  height: 46px;
+  padding: 10px 20px;
+  font-size: 18px;
+  line-height: 1.33333;
+  border-radius: 6px;
+}
+.form-group-lg select.form-control {
+  height: 46px;
+  line-height: 46px;
+}
+.form-group-lg textarea.form-control,
+.form-group-lg select[multiple].form-control {
+  height: auto;
+}
+.form-group-lg .form-control-static {
+  height: 46px;
+  min-height: 38px;
+  padding: 11px 20px;
+  font-size: 18px;
+  line-height: 1.33333;
+}
+
+.has-feedback {
+  position: relative;
+}
+.has-feedback .form-control {
+  padding-right: 52.5px;
+}
+
+.form-control-feedback {
+  position: absolute;
+  top: 0;
+  right: 0;
+  z-index: 2;
+  display: block;
+  width: 42px;
+  height: 42px;
+  line-height: 42px;
+  text-align: center;
+  pointer-events: none;
+}
+
+.input-lg + .form-control-feedback, .input-group-lg > .form-control + .form-control-feedback,
+.input-group-lg > .input-group-addon + .form-control-feedback,
+.input-group-lg > .input-group-btn > .btn + .form-control-feedback,
+.input-group-lg + .form-control-feedback,
+.form-group-lg .form-control + .form-control-feedback {
+  width: 46px;
+  height: 46px;
+  line-height: 46px;
+}
+
+.input-sm + .form-control-feedback, .input-group-sm > .form-control + .form-control-feedback,
+.input-group-sm > .input-group-addon + .form-control-feedback,
+.input-group-sm > .input-group-btn > .btn + .form-control-feedback,
+.input-group-sm + .form-control-feedback,
+.form-group-sm .form-control + .form-control-feedback {
+  width: 30px;
+  height: 30px;
+  line-height: 30px;
+}
+
+.has-success .help-block,
+.has-success .control-label,
+.has-success .radio,
+.has-success .checkbox,
+.has-success .radio-inline,
+.has-success .checkbox-inline, .has-success.radio label, .has-success.checkbox label, .has-success.radio-inline label, .has-success.checkbox-inline label {
+  color: #3c763d;
+}
+.has-success .form-control {
+  border-color: #3c763d;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+.has-success .form-control:focus {
+  border-color: #2b542c;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
+}
+.has-success .input-group-addon {
+  color: #3c763d;
+  border-color: #3c763d;
+  background-color: #dff0d8;
+}
+.has-success .form-control-feedback {
+  color: #3c763d;
+}
+
+.has-warning .help-block,
+.has-warning .control-label,
+.has-warning .radio,
+.has-warning .checkbox,
+.has-warning .radio-inline,
+.has-warning .checkbox-inline, .has-warning.radio label, .has-warning.checkbox label, .has-warning.radio-inline label, .has-warning.checkbox-inline label {
+  color: #8a6d3b;
+}
+.has-warning .form-control {
+  border-color: #8a6d3b;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+.has-warning .form-control:focus {
+  border-color: #66512c;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
+}
+.has-warning .input-group-addon {
+  color: #8a6d3b;
+  border-color: #8a6d3b;
+  background-color: #fcf8e3;
+}
+.has-warning .form-control-feedback {
+  color: #8a6d3b;
+}
+
+.has-error .help-block,
+.has-error .control-label,
+.has-error .radio,
+.has-error .checkbox,
+.has-error .radio-inline,
+.has-error .checkbox-inline, .has-error.radio label, .has-error.checkbox label, .has-error.radio-inline label, .has-error.checkbox-inline label {
+  color: #a94442;
+}
+.has-error .form-control {
+  border-color: #a94442;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+.has-error .form-control:focus {
+  border-color: #843534;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
+}
+.has-error .input-group-addon {
+  color: #a94442;
+  border-color: #a94442;
+  background-color: #f2dede;
+}
+.has-error .form-control-feedback {
+  color: #a94442;
+}
+
+.has-feedback label ~ .form-control-feedback {
+  top: 25px;
+}
+.has-feedback label.sr-only ~ .form-control-feedback {
+  top: 0;
+}
+
+.help-block {
+  display: block;
+  margin-top: 5px;
+  margin-bottom: 10px;
+  color: #737373;
+}
+
+@media (min-width: 768px) {
+  .form-inline .form-group {
+    display: inline-block;
+    margin-bottom: 0;
+    vertical-align: middle;
+  }
+  .form-inline .form-control {
+    display: inline-block;
+    width: auto;
+    vertical-align: middle;
+  }
+  .form-inline .form-control-static {
+    display: inline-block;
+  }
+  .form-inline .input-group {
+    display: inline-table;
+    vertical-align: middle;
+  }
+  .form-inline .input-group .input-group-addon,
+  .form-inline .input-group .input-group-btn,
+  .form-inline .input-group .form-control {
+    width: auto;
+  }
+  .form-inline .input-group > .form-control {
+    width: 100%;
+  }
+  .form-inline .control-label {
+    margin-bottom: 0;
+    vertical-align: middle;
+  }
+  .form-inline .radio,
+  .form-inline .checkbox {
+    display: inline-block;
+    margin-top: 0;
+    margin-bottom: 0;
+    vertical-align: middle;
+  }
+  .form-inline .radio label,
+  .form-inline .checkbox label {
+    padding-left: 0;
+  }
+  .form-inline .radio input[type="radio"],
+  .form-inline .checkbox input[type="checkbox"] {
+    position: relative;
+    margin-left: 0;
+  }
+  .form-inline .has-feedback .form-control-feedback {
+    top: 0;
+  }
+}
+
+.form-horizontal .radio,
+.form-horizontal .checkbox,
+.form-horizontal .radio-inline,
+.form-horizontal .checkbox-inline {
+  margin-top: 0;
+  margin-bottom: 0;
+  padding-top: 11px;
+}
+.form-horizontal .radio,
+.form-horizontal .checkbox {
+  min-height: 31px;
+}
+.form-horizontal .form-group {
+  margin-left: -15px;
+  margin-right: -15px;
+}
+.form-horizontal .form-group:before, .form-horizontal .form-group:after {
+  content: " ";
+  display: table;
+}
+.form-horizontal .form-group:after {
+  clear: both;
+}
+@media (min-width: 768px) {
+  .form-horizontal .control-label {
+    text-align: right;
+    margin-bottom: 0;
+    padding-top: 11px;
+  }
+}
+.form-horizontal .has-feedback .form-control-feedback {
+  right: 15px;
+}
+@media (min-width: 768px) {
+  .form-horizontal .form-group-lg .control-label {
+    padding-top: 14.33333px;
+    font-size: 18px;
+  }
+}
+@media (min-width: 768px) {
+  .form-horizontal .form-group-sm .control-label {
+    padding-top: 6px;
+    font-size: 12px;
+  }
+}
+
+.btn {
+  display: inline-block;
+  margin-bottom: 0;
+  font-weight: normal;
+  text-align: center;
+  vertical-align: middle;
+  touch-action: manipulation;
+  cursor: pointer;
+  background-image: none;
+  border: 1px solid transparent;
+  white-space: nowrap;
+  padding: 10px 20px;
+  font-size: 14px;
+  line-height: 1.42857;
+  border-radius: 4px;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+.btn:focus, .btn.focus, .btn:active:focus, .btn:active.focus, .btn.active:focus, .btn.active.focus {
+  outline: thin dotted;
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
+.btn:hover, .btn:focus, .btn.focus {
+  color: #333;
+  text-decoration: none;
+}
+.btn:active, .btn.active {
+  outline: 0;
+  background-image: none;
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+  box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+}
+.btn.disabled, .btn[disabled], fieldset[disabled] .btn {
+  cursor: not-allowed;
+  opacity: 0.65;
+  filter: alpha(opacity=65);
+  -webkit-box-shadow: none;
+  box-shadow: none;
+}
+
+a.btn.disabled, fieldset[disabled] a.btn {
+  pointer-events: none;
+}
+
+.btn-default {
+  color: #333;
+  background-color: #fff;
+  border-color: #ccc;
+}
+.btn-default:focus, .btn-default.focus {
+  color: #333;
+  background-color: #e6e6e6;
+  border-color: #8c8c8c;
+}
+.btn-default:hover {
+  color: #333;
+  background-color: #e6e6e6;
+  border-color: #adadad;
+}
+.btn-default:active, .btn-default.active, .open > .btn-default.dropdown-toggle {
+  color: #333;
+  background-color: #e6e6e6;
+  border-color: #adadad;
+}
+.btn-default:active:hover, .btn-default:active:focus, .btn-default:active.focus, .btn-default.active:hover, .btn-default.active:focus, .btn-default.active.focus, .open > .btn-default.dropdown-toggle:hover, .open > .btn-default.dropdown-toggle:focus, .open > .btn-default.dropdown-toggle.focus {
+  color: #333;
+  background-color: #d4d4d4;
+  border-color: #8c8c8c;
+}
+.btn-default:active, .btn-default.active, .open > .btn-default.dropdown-toggle {
+  background-image: none;
+}
+.btn-default.disabled, .btn-default.disabled:hover, .btn-default.disabled:focus, .btn-default.disabled.focus, .btn-default.disabled:active, .btn-default.disabled.active, .btn-default[disabled], .btn-default[disabled]:hover, .btn-default[disabled]:focus, .btn-default[disabled].focus, .btn-default[disabled]:active, .btn-default[disabled].active, fieldset[disabled] .btn-default, fieldset[disabled] .btn-default:hover, fieldset[disabled] .btn-default:focus, fieldset[disabled] .btn-default.focus, fieldset[disabled] .btn-default:active, fieldset[disabled] .btn-default.active {
+  background-color: #fff;
+  border-color: #ccc;
+}
+.btn-default .badge {
+  color: #fff;
+  background-color: #333;
+}
+
+.btn-primary {
+  color: #fff;
+  background-color: #337ab7;
+  border-color: #2e6da4;
+}
+.btn-primary:focus, .btn-primary.focus {
+  color: #fff;
+  background-color: #286090;
+  border-color: #122b40;
+}
+.btn-primary:hover {
+  color: #fff;
+  background-color: #286090;
+  border-color: #204d74;
+}
+.btn-primary:active, .btn-primary.active, .open > .btn-primary.dropdown-toggle {
+  color: #fff;
+  background-color: #286090;
+  border-color: #204d74;
+}
+.btn-primary:active:hover, .btn-primary:active:focus, .btn-primary:active.focus, .btn-primary.active:hover, .btn-primary.active:focus, .btn-primary.active.focus, .open > .btn-primary.dropdown-toggle:hover, .open > .btn-primary.dropdown-toggle:focus, .open > .btn-primary.dropdown-toggle.focus {
+  color: #fff;
+  background-color: #204d74;
+  border-color: #122b40;
+}
+.btn-primary:active, .btn-primary.active, .open > .btn-primary.dropdown-toggle {
+  background-image: none;
+}
+.btn-primary.disabled, .btn-primary.disabled:hover, .btn-primary.disabled:focus, .btn-primary.disabled.focus, .btn-primary.disabled:active, .btn-primary.disabled.active, .btn-primary[disabled], .btn-primary[disabled]:hover, .btn-primary[disabled]:focus, .btn-primary[disabled].focus, .btn-primary[disabled]:active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary, fieldset[disabled] .btn-primary:hover, fieldset[disabled] .btn-primary:focus, fieldset[disabled] .btn-primary.focus, fieldset[disabled] .btn-primary:active, fieldset[disabled] .btn-primary.active {
+  background-color: #337ab7;
+  border-color: #2e6da4;
+}
+.btn-primary .badge {
+  color: #337ab7;
+  background-color: #fff;
+}
+
+.btn-success {
+  color: #fff;
+  background-color: #5cb85c;
+  border-color: #4cae4c;
+}
+.btn-success:focus, .btn-success.focus {
+  color: #fff;
+  background-color: #449d44;
+  border-color: #255625;
+}
+.btn-success:hover {
+  color: #fff;
+  background-color: #449d44;
+  border-color: #398439;
+}
+.btn-success:active, .btn-success.active, .open > .btn-success.dropdown-toggle {
+  color: #fff;
+  background-color: #449d44;
+  border-color: #398439;
+}
+.btn-success:active:hover, .btn-success:active:focus, .btn-success:active.focus, .btn-success.active:hover, .btn-success.active:focus, .btn-success.active.focus, .open > .btn-success.dropdown-toggle:hover, .open > .btn-success.dropdown-toggle:focus, .open > .btn-success.dropdown-toggle.focus {
+  color: #fff;
+  background-color: #398439;
+  border-color: #255625;
+}
+.btn-success:active, .btn-success.active, .open > .btn-success.dropdown-toggle {
+  background-image: none;
+}
+.btn-success.disabled, .btn-success.disabled:hover, .btn-success.disabled:focus, .btn-success.disabled.focus, .btn-success.disabled:active, .btn-success.disabled.active, .btn-success[disabled], .btn-success[disabled]:hover, .btn-success[disabled]:focus, .btn-success[disabled].focus, .btn-success[disabled]:active, .btn-success[disabled].active, fieldset[disabled] .btn-success, fieldset[disabled] .btn-success:hover, fieldset[disabled] .btn-success:focus, fieldset[disabled] .btn-success.focus, fieldset[disabled] .btn-success:active, fieldset[disabled] .btn-success.active {
+  background-color: #5cb85c;
+  border-color: #4cae4c;
+}
+.btn-success .badge {
+  color: #5cb85c;
+  background-color: #fff;
+}
+
+.btn-info {
+  color: #fff;
+  background-color: #5bc0de;
+  border-color: #46b8da;
+}
+.btn-info:focus, .btn-info.focus {
+  color: #fff;
+  background-color: #31b0d5;
+  border-color: #1b6d85;
+}
+.btn-info:hover {
+  color: #fff;
+  background-color: #31b0d5;
+  border-color: #269abc;
+}
+.btn-info:active, .btn-info.active, .open > .btn-info.dropdown-toggle {
+  color: #fff;
+  background-color: #31b0d5;
+  border-color: #269abc;
+}
+.btn-info:active:hover, .btn-info:active:focus, .btn-info:active.focus, .btn-info.active:hover, .btn-info.active:focus, .btn-info.active.focus, .open > .btn-info.dropdown-toggle:hover, .open > .btn-info.dropdown-toggle:focus, .open > .btn-info.dropdown-toggle.focus {
+  color: #fff;
+  background-color: #269abc;
+  border-color: #1b6d85;
+}
+.btn-info:active, .btn-info.active, .open > .btn-info.dropdown-toggle {
+  background-image: none;
+}
+.btn-info.disabled, .btn-info.disabled:hover, .btn-info.disabled:focus, .btn-info.disabled.focus, .btn-info.disabled:active, .btn-info.disabled.active, .btn-info[disabled], .btn-info[disabled]:hover, .btn-info[disabled]:focus, .btn-info[disabled].focus, .btn-info[disabled]:active, .btn-info[disabled].active, fieldset[disabled] .btn-info, fieldset[disabled] .btn-info:hover, fieldset[disabled] .btn-info:focus, fieldset[disabled] .btn-info.focus, fieldset[disabled] .btn-info:active, fieldset[disabled] .btn-info.active {
+  background-color: #5bc0de;
+  border-color: #46b8da;
+}
+.btn-info .badge {
+  color: #5bc0de;
+  background-color: #fff;
+}
+
+.btn-warning {
+  color: #fff;
+  background-color: #f0ad4e;
+  border-color: #eea236;
+}
+.btn-warning:focus, .btn-warning.focus {
+  color: #fff;
+  background-color: #ec971f;
+  border-color: #985f0d;
+}
+.btn-warning:hover {
+  color: #fff;
+  background-color: #ec971f;
+  border-color: #d58512;
+}
+.btn-warning:active, .btn-warning.active, .open > .btn-warning.dropdown-toggle {
+  color: #fff;
+  background-color: #ec971f;
+  border-color: #d58512;
+}
+.btn-warning:active:hover, .btn-warning:active:focus, .btn-warning:active.focus, .btn-warning.active:hover, .btn-warning.active:focus, .btn-warning.active.focus, .open > .btn-warning.dropdown-toggle:hover, .open > .btn-warning.dropdown-toggle:focus, .open > .btn-warning.dropdown-toggle.focus {
+  color: #fff;
+  background-color: #d58512;
+  border-color: #985f0d;
+}
+.btn-warning:active, .btn-warning.active, .open > .btn-warning.dropdown-toggle {
+  background-image: none;
+}
+.btn-warning.disabled, .btn-warning.disabled:hover, .btn-warning.disabled:focus, .btn-warning.disabled.focus, .btn-warning.disabled:active, .btn-warning.disabled.active, .btn-warning[disabled], .btn-warning[disabled]:hover, .btn-warning[disabled]:focus, .btn-warning[disabled].focus, .btn-warning[disabled]:active, .btn-warning[disabled].active, fieldset[disabled] .btn-warning, fieldset[disabled] .btn-warning:hover, fieldset[disabled] .btn-warning:focus, fieldset[disabled] .btn-warning.focus, fieldset[disabled] .btn-warning:active, fieldset[disabled] .btn-warning.active {
+  background-color: #f0ad4e;
+  border-color: #eea236;
+}
+.btn-warning .badge {
+  color: #f0ad4e;
+  background-color: #fff;
+}
+
+.btn-danger {
+  color: #fff;
+  background-color: #d9534f;
+  border-color: #d43f3a;
+}
+.btn-danger:focus, .btn-danger.focus {
+  color: #fff;
+  background-color: #c9302c;
+  border-color: #761c19;
+}
+.btn-danger:hover {
+  color: #fff;
+  background-color: #c9302c;
+  border-color: #ac2925;
+}
+.btn-danger:active, .btn-danger.active, .open > .btn-danger.dropdown-toggle {
+  color: #fff;
+  background-color: #c9302c;
+  border-color: #ac2925;
+}
+.btn-danger:active:hover, .btn-danger:active:focus, .btn-danger:active.focus, .btn-danger.active:hover, .btn-danger.active:focus, .btn-danger.active.focus, .open > .btn-danger.dropdown-toggle:hover, .open > .btn-danger.dropdown-toggle:focus, .open > .btn-danger.dropdown-toggle.focus {
+  color: #fff;
+  background-color: #ac2925;
+  border-color: #761c19;
+}
+.btn-danger:active, .btn-danger.active, .open > .btn-danger.dropdown-toggle {
+  background-image: none;
+}
+.btn-danger.disabled, .btn-danger.disabled:hover, .btn-danger.disabled:focus, .btn-danger.disabled.focus, .btn-danger.disabled:active, .btn-danger.disabled.active, .btn-danger[disabled], .btn-danger[disabled]:hover, .btn-danger[disabled]:focus, .btn-danger[disabled].focus, .btn-danger[disabled]:active, .btn-danger[disabled].active, fieldset[disabled] .btn-danger, fieldset[disabled] .btn-danger:hover, fieldset[disabled] .btn-danger:focus, fieldset[disabled] .btn-danger.focus, fieldset[disabled] .btn-danger:active, fieldset[disabled] .btn-danger.active {
+  background-color: #d9534f;
+  border-color: #d43f3a;
+}
+.btn-danger .badge {
+  color: #d9534f;
+  background-color: #fff;
+}
+
+.btn-link {
+  color: #337ab7;
+  font-weight: normal;
+  border-radius: 0;
+}
+.btn-link, .btn-link:active, .btn-link.active, .btn-link[disabled], fieldset[disabled] .btn-link {
+  background-color: transparent;
+  -webkit-box-shadow: none;
+  box-shadow: none;
+}
+.btn-link, .btn-link:hover, .btn-link:focus, .btn-link:active {
+  border-color: transparent;
+}
+.btn-link:hover, .btn-link:focus {
+  color: #23527c;
+  text-decoration: underline;
+  background-color: transparent;
+}
+.btn-link[disabled]:hover, .btn-link[disabled]:focus, fieldset[disabled] .btn-link:hover, fieldset[disabled] .btn-link:focus {
+  color: #777777;
+  text-decoration: none;
+}
+
+.btn-lg, .btn-group-lg > .btn {
+  padding: 10px 20px;
+  font-size: 18px;
+  line-height: 1.33333;
+  border-radius: 6px;
+}
+
+.btn-sm, .btn-group-sm > .btn {
+  padding: 5px 20px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+
+.btn-xs, .btn-group-xs > .btn {
+  padding: 1px 5px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+
+.btn-block {
+  display: block;
+  width: 100%;
+}
+
+.btn-block + .btn-block {
+  margin-top: 5px;
+}
+
+input[type="submit"].btn-block,
+input[type="reset"].btn-block,
+input[type="button"].btn-block {
+  width: 100%;
+}
+
+.fade {
+  opacity: 0;
+  -webkit-transition: opacity 0.15s linear;
+  -o-transition: opacity 0.15s linear;
+  transition: opacity 0.15s linear;
+}
+.fade.in {
+  opacity: 1;
+}
+
+.collapse {
+  display: none;
+}
+.collapse.in {
+  display: block;
+}
+
+tr.collapse.in {
+  display: table-row;
+}
+
+tbody.collapse.in {
+  display: table-row-group;
+}
+
+.collapsing {
+  position: relative;
+  height: 0;
+  overflow: hidden;
+  -webkit-transition-property: height, visibility;
+  transition-property: height, visibility;
+  -webkit-transition-duration: 0.35s;
+  transition-duration: 0.35s;
+  -webkit-transition-timing-function: ease;
+  transition-timing-function: ease;
+}
+
+.caret {
+  display: inline-block;
+  width: 0;
+  height: 0;
+  margin-left: 2px;
+  vertical-align: middle;
+  border-top: 4px dashed;
+  border-top: 4px solid \9;
+  border-right: 4px solid transparent;
+  border-left: 4px solid transparent;
+}
+
+.dropup,
+.dropdown {
+  position: relative;
+}
+
+.dropdown-toggle:focus {
+  outline: 0;
+}
+
+.dropdown-menu {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  z-index: 1000;
+  display: none;
+  float: left;
+  min-width: 160px;
+  padding: 5px 0;
+  margin: 2px 0 0;
+  list-style: none;
+  font-size: 14px;
+  text-align: left;
+  background-color: #fff;
+  border: 1px solid #ccc;
+  border: 1px solid rgba(0, 0, 0, 0.15);
+  border-radius: 4px;
+  -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+  background-clip: padding-box;
+}
+.dropdown-menu.pull-right {
+  right: 0;
+  left: auto;
+}
+.dropdown-menu .divider {
+  height: 1px;
+  margin: 9px 0;
+  overflow: hidden;
+  background-color: #e5e5e5;
+}
+.dropdown-menu > li > a {
+  display: block;
+  padding: 3px 20px;
+  clear: both;
+  font-weight: normal;
+  line-height: 1.42857;
+  color: #333333;
+  white-space: nowrap;
+}
+
+.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus {
+  text-decoration: none;
+  color: #262626;
+  background-color: #f5f5f5;
+}
+
+.dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:focus {
+  color: #fff;
+  text-decoration: none;
+  outline: 0;
+  background-color: #337ab7;
+}
+
+.dropdown-menu > .disabled > a, .dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus {
+  color: #777777;
+}
+.dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus {
+  text-decoration: none;
+  background-color: transparent;
+  background-image: none;
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  cursor: not-allowed;
+}
+
+.open > .dropdown-menu {
+  display: block;
+}
+.open > a {
+  outline: 0;
+}
+
+.dropdown-menu-right {
+  left: auto;
+  right: 0;
+}
+
+.dropdown-menu-left {
+  left: 0;
+  right: auto;
+}
+
+.dropdown-header {
+  display: block;
+  padding: 3px 20px;
+  font-size: 12px;
+  line-height: 1.42857;
+  color: #777777;
+  white-space: nowrap;
+}
+
+.dropdown-backdrop {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  top: 0;
+  z-index: 990;
+}
+
+.pull-right > .dropdown-menu {
+  right: 0;
+  left: auto;
+}
+
+.dropup .caret,
+.navbar-fixed-bottom .dropdown .caret {
+  border-top: 0;
+  border-bottom: 4px dashed;
+  border-bottom: 4px solid \9;
+  content: "";
+}
+.dropup .dropdown-menu,
+.navbar-fixed-bottom .dropdown .dropdown-menu {
+  top: auto;
+  bottom: 100%;
+  margin-bottom: 2px;
+}
+
+@media (min-width: 768px) {
+  .navbar-right .dropdown-menu {
+    right: 0;
+    left: auto;
+  }
+  .navbar-right .dropdown-menu-left {
+    left: 0;
+    right: auto;
+  }
+}
+.btn-group,
+.btn-group-vertical {
+  position: relative;
+  display: inline-block;
+  vertical-align: middle;
+}
+.btn-group > .btn,
+.btn-group-vertical > .btn {
+  position: relative;
+  float: left;
+}
+.btn-group > .btn:hover, .btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active,
+.btn-group-vertical > .btn:hover,
+.btn-group-vertical > .btn:focus,
+.btn-group-vertical > .btn:active,
+.btn-group-vertical > .btn.active {
+  z-index: 2;
+}
+
+.btn-group .btn + .btn,
+.btn-group .btn + .btn-group,
+.btn-group .btn-group + .btn,
+.btn-group .btn-group + .btn-group {
+  margin-left: -1px;
+}
+
+.btn-toolbar {
+  margin-left: -5px;
+}
+.btn-toolbar:before, .btn-toolbar:after {
+  content: " ";
+  display: table;
+}
+.btn-toolbar:after {
+  clear: both;
+}
+.btn-toolbar .btn,
+.btn-toolbar .btn-group,
+.btn-toolbar .input-group {
+  float: left;
+}
+.btn-toolbar > .btn,
+.btn-toolbar > .btn-group,
+.btn-toolbar > .input-group {
+  margin-left: 5px;
+}
+
+.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {
+  border-radius: 0;
+}
+
+.btn-group > .btn:first-child {
+  margin-left: 0;
+}
+.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {
+  border-bottom-right-radius: 0;
+  border-top-right-radius: 0;
+}
+
+.btn-group > .btn:last-child:not(:first-child),
+.btn-group > .dropdown-toggle:not(:first-child) {
+  border-bottom-left-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.btn-group > .btn-group {
+  float: left;
+}
+
+.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {
+  border-radius: 0;
+}
+
+.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,
+.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {
+  border-bottom-right-radius: 0;
+  border-top-right-radius: 0;
+}
+
+.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {
+  border-bottom-left-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.btn-group .dropdown-toggle:active,
+.btn-group.open .dropdown-toggle {
+  outline: 0;
+}
+
+.btn-group > .btn + .dropdown-toggle {
+  padding-left: 8px;
+  padding-right: 8px;
+}
+
+.btn-group > .btn-lg + .dropdown-toggle, .btn-group-lg.btn-group > .btn + .dropdown-toggle {
+  padding-left: 12px;
+  padding-right: 12px;
+}
+
+.btn-group.open .dropdown-toggle {
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+  box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+}
+.btn-group.open .dropdown-toggle.btn-link {
+  -webkit-box-shadow: none;
+  box-shadow: none;
+}
+
+.btn .caret {
+  margin-left: 0;
+}
+
+.btn-lg .caret, .btn-group-lg > .btn .caret {
+  border-width: 5px 5px 0;
+  border-bottom-width: 0;
+}
+
+.dropup .btn-lg .caret, .dropup .btn-group-lg > .btn .caret {
+  border-width: 0 5px 5px;
+}
+
+.btn-group-vertical > .btn,
+.btn-group-vertical > .btn-group,
+.btn-group-vertical > .btn-group > .btn {
+  display: block;
+  float: none;
+  width: 100%;
+  max-width: 100%;
+}
+.btn-group-vertical > .btn-group:before, .btn-group-vertical > .btn-group:after {
+  content: " ";
+  display: table;
+}
+.btn-group-vertical > .btn-group:after {
+  clear: both;
+}
+.btn-group-vertical > .btn-group > .btn {
+  float: none;
+}
+.btn-group-vertical > .btn + .btn,
+.btn-group-vertical > .btn + .btn-group,
+.btn-group-vertical > .btn-group + .btn,
+.btn-group-vertical > .btn-group + .btn-group {
+  margin-top: -1px;
+  margin-left: 0;
+}
+
+.btn-group-vertical > .btn:not(:first-child):not(:last-child) {
+  border-radius: 0;
+}
+.btn-group-vertical > .btn:first-child:not(:last-child) {
+  border-top-right-radius: 4px;
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+.btn-group-vertical > .btn:last-child:not(:first-child) {
+  border-bottom-left-radius: 4px;
+  border-top-right-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {
+  border-radius: 0;
+}
+
+.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,
+.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {
+  border-top-right-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.btn-group-justified {
+  display: table;
+  width: 100%;
+  table-layout: fixed;
+  border-collapse: separate;
+}
+.btn-group-justified > .btn,
+.btn-group-justified > .btn-group {
+  float: none;
+  display: table-cell;
+  width: 1%;
+}
+.btn-group-justified > .btn-group .btn {
+  width: 100%;
+}
+.btn-group-justified > .btn-group .dropdown-menu {
+  left: auto;
+}
+
+[data-toggle="buttons"] > .btn input[type="radio"],
+[data-toggle="buttons"] > .btn input[type="checkbox"],
+[data-toggle="buttons"] > .btn-group > .btn input[type="radio"],
+[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] {
+  position: absolute;
+  clip: rect(0, 0, 0, 0);
+  pointer-events: none;
+}
+
+.input-group {
+  position: relative;
+  display: table;
+  border-collapse: separate;
+}
+.input-group[class*="col-"] {
+  float: none;
+  padding-left: 0;
+  padding-right: 0;
+}
+.input-group .form-control {
+  position: relative;
+  z-index: 2;
+  float: left;
+  width: 100%;
+  margin-bottom: 0;
+}
+
+.input-group-addon,
+.input-group-btn,
+.input-group .form-control {
+  display: table-cell;
+}
+.input-group-addon:not(:first-child):not(:last-child),
+.input-group-btn:not(:first-child):not(:last-child),
+.input-group .form-control:not(:first-child):not(:last-child) {
+  border-radius: 0;
+}
+
+.input-group-addon,
+.input-group-btn {
+  width: 1%;
+  white-space: nowrap;
+  vertical-align: middle;
+}
+
+.input-group-addon {
+  padding: 10px 20px;
+  font-size: 14px;
+  font-weight: normal;
+  line-height: 1;
+  color: #555555;
+  text-align: center;
+  background-color: #eeeeee;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+}
+.input-group-addon.input-sm,
+.input-group-sm > .input-group-addon,
+.input-group-sm > .input-group-btn > .input-group-addon.btn {
+  padding: 5px 20px;
+  font-size: 12px;
+  border-radius: 3px;
+}
+.input-group-addon.input-lg,
+.input-group-lg > .input-group-addon,
+.input-group-lg > .input-group-btn > .input-group-addon.btn {
+  padding: 10px 20px;
+  font-size: 18px;
+  border-radius: 6px;
+}
+.input-group-addon input[type="radio"],
+.input-group-addon input[type="checkbox"] {
+  margin-top: 0;
+}
+
+.input-group .form-control:first-child,
+.input-group-addon:first-child,
+.input-group-btn:first-child > .btn,
+.input-group-btn:first-child > .btn-group > .btn,
+.input-group-btn:first-child > .dropdown-toggle,
+.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),
+.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {
+  border-bottom-right-radius: 0;
+  border-top-right-radius: 0;
+}
+
+.input-group-addon:first-child {
+  border-right: 0;
+}
+
+.input-group .form-control:last-child,
+.input-group-addon:last-child,
+.input-group-btn:last-child > .btn,
+.input-group-btn:last-child > .btn-group > .btn,
+.input-group-btn:last-child > .dropdown-toggle,
+.input-group-btn:first-child > .btn:not(:first-child),
+.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {
+  border-bottom-left-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.input-group-addon:last-child {
+  border-left: 0;
+}
+
+.input-group-btn {
+  position: relative;
+  font-size: 0;
+  white-space: nowrap;
+}
+.input-group-btn > .btn {
+  position: relative;
+}
+.input-group-btn > .btn + .btn {
+  margin-left: -1px;
+}
+.input-group-btn > .btn:hover, .input-group-btn > .btn:focus, .input-group-btn > .btn:active {
+  z-index: 2;
+}
+.input-group-btn:first-child > .btn,
+.input-group-btn:first-child > .btn-group {
+  margin-right: -1px;
+}
+.input-group-btn:last-child > .btn,
+.input-group-btn:last-child > .btn-group {
+  z-index: 2;
+  margin-left: -1px;
+}
+
+.nav {
+  margin-bottom: 0;
+  padding-left: 0;
+  list-style: none;
+}
+.nav:before, .nav:after {
+  content: " ";
+  display: table;
+}
+.nav:after {
+  clear: both;
+}
+.nav > li {
+  position: relative;
+  display: block;
+}
+.nav > li > a {
+  position: relative;
+  display: block;
+  padding: 10px 15px;
+}
+.nav > li > a:hover, .nav > li > a:focus {
+  text-decoration: none;
+  background-color: #eeeeee;
+}
+.nav > li.disabled > a {
+  color: #777777;
+}
+.nav > li.disabled > a:hover, .nav > li.disabled > a:focus {
+  color: #777777;
+  text-decoration: none;
+  background-color: transparent;
+  cursor: not-allowed;
+}
+.nav .open > a, .nav .open > a:hover, .nav .open > a:focus {
+  background-color: #eeeeee;
+  border-color: #337ab7;
+}
+.nav .nav-divider {
+  height: 1px;
+  margin: 9px 0;
+  overflow: hidden;
+  background-color: #e5e5e5;
+}
+.nav > li > a > img {
+  max-width: none;
+}
+
+.nav-tabs {
+  border-bottom: 1px solid #ddd;
+}
+.nav-tabs > li {
+  float: left;
+  margin-bottom: -1px;
+}
+.nav-tabs > li > a {
+  margin-right: 2px;
+  line-height: 1.42857;
+  border: 1px solid transparent;
+  border-radius: 4px 4px 0 0;
+}
+.nav-tabs > li > a:hover {
+  border-color: #eeeeee #eeeeee #ddd;
+}
+.nav-tabs > li.active > a, .nav-tabs > li.active > a:hover, .nav-tabs > li.active > a:focus {
+  color: #555555;
+  background-color: #fff;
+  border: 1px solid #ddd;
+  border-bottom-color: transparent;
+  cursor: default;
+}
+
+.nav-pills > li {
+  float: left;
+}
+.nav-pills > li > a {
+  border-radius: 4px;
+}
+.nav-pills > li + li {
+  margin-left: 2px;
+}
+.nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus {
+  color: #fff;
+  background-color: #337ab7;
+}
+
+.nav-stacked > li {
+  float: none;
+}
+.nav-stacked > li + li {
+  margin-top: 2px;
+  margin-left: 0;
+}
+
+.nav-justified, .nav-tabs.nav-justified {
+  width: 100%;
+}
+.nav-justified > li, .nav-tabs.nav-justified > li {
+  float: none;
+}
+.nav-justified > li > a, .nav-tabs.nav-justified > li > a {
+  text-align: center;
+  margin-bottom: 5px;
+}
+.nav-justified > .dropdown .dropdown-menu {
+  top: auto;
+  left: auto;
+}
+@media (min-width: 768px) {
+  .nav-justified > li, .nav-tabs.nav-justified > li {
+    display: table-cell;
+    width: 1%;
+  }
+  .nav-justified > li > a, .nav-tabs.nav-justified > li > a {
+    margin-bottom: 0;
+  }
+}
+
+.nav-tabs-justified, .nav-tabs.nav-justified {
+  border-bottom: 0;
+}
+.nav-tabs-justified > li > a, .nav-tabs.nav-justified > li > a {
+  margin-right: 0;
+  border-radius: 4px;
+}
+.nav-tabs-justified > .active > a, .nav-tabs.nav-justified > .active > a,
+.nav-tabs-justified > .active > a:hover,
+.nav-tabs.nav-justified > .active > a:hover,
+.nav-tabs-justified > .active > a:focus,
+.nav-tabs.nav-justified > .active > a:focus {
+  border: 1px solid #ddd;
+}
+@media (min-width: 768px) {
+  .nav-tabs-justified > li > a, .nav-tabs.nav-justified > li > a {
+    border-bottom: 1px solid #ddd;
+    border-radius: 4px 4px 0 0;
+  }
+  .nav-tabs-justified > .active > a, .nav-tabs.nav-justified > .active > a,
+  .nav-tabs-justified > .active > a:hover,
+  .nav-tabs.nav-justified > .active > a:hover,
+  .nav-tabs-justified > .active > a:focus,
+  .nav-tabs.nav-justified > .active > a:focus {
+    border-bottom-color: #fff;
+  }
+}
+
+.tab-content > .tab-pane {
+  display: none;
+}
+.tab-content > .active {
+  display: block;
+}
+
+.nav-tabs .dropdown-menu {
+  margin-top: -1px;
+  border-top-right-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.navbar {
+  position: relative;
+  min-height: 55px;
+  margin-bottom: 20px;
+  border: 1px solid transparent;
+}
+.navbar:before, .navbar:after {
+  content: " ";
+  display: table;
+}
+.navbar:after {
+  clear: both;
+}
+@media (min-width: 768px) {
+  .navbar {
+    border-radius: 4px;
+  }
+}
+
+.navbar-header:before, .navbar-header:after {
+  content: " ";
+  display: table;
+}
+.navbar-header:after {
+  clear: both;
+}
+@media (min-width: 768px) {
+  .navbar-header {
+    float: left;
+  }
+}
+
+.navbar-collapse {
+  overflow-x: visible;
+  padding-right: 15px;
+  padding-left: 15px;
+  border-top: 1px solid transparent;
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
+  -webkit-overflow-scrolling: touch;
+}
+.navbar-collapse:before, .navbar-collapse:after {
+  content: " ";
+  display: table;
+}
+.navbar-collapse:after {
+  clear: both;
+}
+.navbar-collapse.in {
+  overflow-y: auto;
+}
+@media (min-width: 768px) {
+  .navbar-collapse {
+    width: auto;
+    border-top: 0;
+    box-shadow: none;
+  }
+  .navbar-collapse.collapse {
+    display: block !important;
+    height: auto !important;
+    padding-bottom: 0;
+    overflow: visible !important;
+  }
+  .navbar-collapse.in {
+    overflow-y: visible;
+  }
+  .navbar-fixed-top .navbar-collapse, .navbar-static-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse {
+    padding-left: 0;
+    padding-right: 0;
+  }
+}
+
+.navbar-fixed-top .navbar-collapse,
+.navbar-fixed-bottom .navbar-collapse {
+  max-height: 340px;
+}
+@media (max-device-width: 480px) and (orientation: landscape) {
+  .navbar-fixed-top .navbar-collapse,
+  .navbar-fixed-bottom .navbar-collapse {
+    max-height: 200px;
+  }
+}
+
+.container > .navbar-header,
+.container > .navbar-collapse,
+.container-fluid > .navbar-header,
+.container-fluid > .navbar-collapse {
+  margin-right: -15px;
+  margin-left: -15px;
+}
+@media (min-width: 768px) {
+  .container > .navbar-header,
+  .container > .navbar-collapse,
+  .container-fluid > .navbar-header,
+  .container-fluid > .navbar-collapse {
+    margin-right: 0;
+    margin-left: 0;
+  }
+}
+
+.navbar-static-top {
+  z-index: 1000;
+  border-width: 0 0 1px;
+}
+@media (min-width: 768px) {
+  .navbar-static-top {
+    border-radius: 0;
+  }
+}
+
+.navbar-fixed-top,
+.navbar-fixed-bottom {
+  position: fixed;
+  right: 0;
+  left: 0;
+  z-index: 1030;
+}
+@media (min-width: 768px) {
+  .navbar-fixed-top,
+  .navbar-fixed-bottom {
+    border-radius: 0;
+  }
+}
+
+.navbar-fixed-top {
+  top: 0;
+  border-width: 0 0 1px;
+}
+
+.navbar-fixed-bottom {
+  bottom: 0;
+  margin-bottom: 0;
+  border-width: 1px 0 0;
+}
+
+.navbar-brand {
+  float: left;
+  padding: 17.5px 15px;
+  font-size: 18px;
+  line-height: 20px;
+  height: 55px;
+}
+.navbar-brand:hover, .navbar-brand:focus {
+  text-decoration: none;
+}
+.navbar-brand > img {
+  display: block;
+}
+@media (min-width: 768px) {
+  .navbar > .container .navbar-brand, .navbar > .container-fluid .navbar-brand {
+    margin-left: -15px;
+  }
+}
+
+.navbar-toggle {
+  position: relative;
+  float: right;
+  margin-right: 15px;
+  padding: 9px 10px;
+  margin-top: 10.5px;
+  margin-bottom: 10.5px;
+  background-color: transparent;
+  background-image: none;
+  border: 1px solid transparent;
+  border-radius: 4px;
+}
+.navbar-toggle:focus {
+  outline: 0;
+}
+.navbar-toggle .icon-bar {
+  display: block;
+  width: 22px;
+  height: 2px;
+  border-radius: 1px;
+}
+.navbar-toggle .icon-bar + .icon-bar {
+  margin-top: 4px;
+}
+@media (min-width: 768px) {
+  .navbar-toggle {
+    display: none;
+  }
+}
+
+.navbar-nav {
+  margin: 8.75px -15px;
+}
+.navbar-nav > li > a {
+  padding-top: 10px;
+  padding-bottom: 10px;
+  line-height: 20px;
+}
+@media (max-width: 767px) {
+  .navbar-nav .open .dropdown-menu {
+    position: static;
+    float: none;
+    width: auto;
+    margin-top: 0;
+    background-color: transparent;
+    border: 0;
+    box-shadow: none;
+  }
+  .navbar-nav .open .dropdown-menu > li > a,
+  .navbar-nav .open .dropdown-menu .dropdown-header {
+    padding: 5px 15px 5px 25px;
+  }
+  .navbar-nav .open .dropdown-menu > li > a {
+    line-height: 20px;
+  }
+  .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-nav .open .dropdown-menu > li > a:focus {
+    background-image: none;
+  }
+}
+@media (min-width: 768px) {
+  .navbar-nav {
+    float: left;
+    margin: 0;
+  }
+  .navbar-nav > li {
+    float: left;
+  }
+  .navbar-nav > li > a {
+    padding-top: 17.5px;
+    padding-bottom: 17.5px;
+  }
+}
+
+.navbar-form {
+  margin-left: -15px;
+  margin-right: -15px;
+  padding: 10px 15px;
+  border-top: 1px solid transparent;
+  border-bottom: 1px solid transparent;
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
+  margin-top: 6.5px;
+  margin-bottom: 6.5px;
+}
+@media (min-width: 768px) {
+  .navbar-form .form-group {
+    display: inline-block;
+    margin-bottom: 0;
+    vertical-align: middle;
+  }
+  .navbar-form .form-control {
+    display: inline-block;
+    width: auto;
+    vertical-align: middle;
+  }
+  .navbar-form .form-control-static {
+    display: inline-block;
+  }
+  .navbar-form .input-group {
+    display: inline-table;
+    vertical-align: middle;
+  }
+  .navbar-form .input-group .input-group-addon,
+  .navbar-form .input-group .input-group-btn,
+  .navbar-form .input-group .form-control {
+    width: auto;
+  }
+  .navbar-form .input-group > .form-control {
+    width: 100%;
+  }
+  .navbar-form .control-label {
+    margin-bottom: 0;
+    vertical-align: middle;
+  }
+  .navbar-form .radio,
+  .navbar-form .checkbox {
+    display: inline-block;
+    margin-top: 0;
+    margin-bottom: 0;
+    vertical-align: middle;
+  }
+  .navbar-form .radio label,
+  .navbar-form .checkbox label {
+    padding-left: 0;
+  }
+  .navbar-form .radio input[type="radio"],
+  .navbar-form .checkbox input[type="checkbox"] {
+    position: relative;
+    margin-left: 0;
+  }
+  .navbar-form .has-feedback .form-control-feedback {
+    top: 0;
+  }
+}
+@media (max-width: 767px) {
+  .navbar-form .form-group {
+    margin-bottom: 5px;
+  }
+  .navbar-form .form-group:last-child {
+    margin-bottom: 0;
+  }
+}
+@media (min-width: 768px) {
+  .navbar-form {
+    width: auto;
+    border: 0;
+    margin-left: 0;
+    margin-right: 0;
+    padding-top: 0;
+    padding-bottom: 0;
+    -webkit-box-shadow: none;
+    box-shadow: none;
+  }
+}
+
+.navbar-nav > li > .dropdown-menu {
+  margin-top: 0;
+  border-top-right-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {
+  margin-bottom: 0;
+  border-top-right-radius: 4px;
+  border-top-left-radius: 4px;
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+.navbar-btn {
+  margin-top: 6.5px;
+  margin-bottom: 6.5px;
+}
+.navbar-btn.btn-sm, .btn-group-sm > .navbar-btn.btn {
+  margin-top: 12.5px;
+  margin-bottom: 12.5px;
+}
+.navbar-btn.btn-xs, .btn-group-xs > .navbar-btn.btn {
+  margin-top: 16.5px;
+  margin-bottom: 16.5px;
+}
+
+.navbar-text {
+  margin-top: 17.5px;
+  margin-bottom: 17.5px;
+}
+@media (min-width: 768px) {
+  .navbar-text {
+    float: left;
+    margin-left: 15px;
+    margin-right: 15px;
+  }
+}
+
+@media (min-width: 768px) {
+  .navbar-left {
+    float: left !important;
+  }
+
+  .navbar-right {
+    float: right !important;
+    margin-right: -15px;
+  }
+  .navbar-right ~ .navbar-right {
+    margin-right: 0;
+  }
+}
+.navbar-default {
+  background-color: #f8f8f8;
+  border-color: #e7e7e7;
+}
+.navbar-default .navbar-brand {
+  color: #777;
+}
+.navbar-default .navbar-brand:hover, .navbar-default .navbar-brand:focus {
+  color: #5e5e5e;
+  background-color: transparent;
+}
+.navbar-default .navbar-text {
+  color: #777;
+}
+.navbar-default .navbar-nav > li > a {
+  color: #777;
+}
+.navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus {
+  color: #333;
+  background-color: transparent;
+}
+.navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus {
+  color: #555;
+  background-color: #e7e7e7;
+}
+.navbar-default .navbar-nav > .disabled > a, .navbar-default .navbar-nav > .disabled > a:hover, .navbar-default .navbar-nav > .disabled > a:focus {
+  color: #ccc;
+  background-color: transparent;
+}
+.navbar-default .navbar-toggle {
+  border-color: #ddd;
+}
+.navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus {
+  background-color: #ddd;
+}
+.navbar-default .navbar-toggle .icon-bar {
+  background-color: #888;
+}
+.navbar-default .navbar-collapse,
+.navbar-default .navbar-form {
+  border-color: #e7e7e7;
+}
+.navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus {
+  background-color: #e7e7e7;
+  color: #555;
+}
+@media (max-width: 767px) {
+  .navbar-default .navbar-nav .open .dropdown-menu > li > a {
+    color: #777;
+  }
+  .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {
+    color: #333;
+    background-color: transparent;
+  }
+  .navbar-default .navbar-nav .open .dropdown-menu > .active > a, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {
+    color: #555;
+    background-color: #e7e7e7;
+  }
+  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {
+    color: #ccc;
+    background-color: transparent;
+  }
+}
+.navbar-default .navbar-link {
+  color: #777;
+}
+.navbar-default .navbar-link:hover {
+  color: #333;
+}
+.navbar-default .btn-link {
+  color: #777;
+}
+.navbar-default .btn-link:hover, .navbar-default .btn-link:focus {
+  color: #333;
+}
+.navbar-default .btn-link[disabled]:hover, .navbar-default .btn-link[disabled]:focus, fieldset[disabled] .navbar-default .btn-link:hover, fieldset[disabled] .navbar-default .btn-link:focus {
+  color: #ccc;
+}
+
+.navbar-inverse {
+  background-color: #222;
+  border-color: #090909;
+}
+.navbar-inverse .navbar-brand {
+  color: #9d9d9d;
+}
+.navbar-inverse .navbar-brand:hover, .navbar-inverse .navbar-brand:focus {
+  color: #fff;
+  background-color: transparent;
+}
+.navbar-inverse .navbar-text {
+  color: #9d9d9d;
+}
+.navbar-inverse .navbar-nav > li > a {
+  color: #9d9d9d;
+}
+.navbar-inverse .navbar-nav > li > a:hover, .navbar-inverse .navbar-nav > li > a:focus {
+  color: #fff;
+  background-color: transparent;
+}
+.navbar-inverse .navbar-nav > .active > a, .navbar-inverse .navbar-nav > .active > a:hover, .navbar-inverse .navbar-nav > .active > a:focus {
+  color: #fff;
+  background-color: #090909;
+}
+.navbar-inverse .navbar-nav > .disabled > a, .navbar-inverse .navbar-nav > .disabled > a:hover, .navbar-inverse .navbar-nav > .disabled > a:focus {
+  color: #444;
+  background-color: transparent;
+}
+.navbar-inverse .navbar-toggle {
+  border-color: #333;
+}
+.navbar-inverse .navbar-toggle:hover, .navbar-inverse .navbar-toggle:focus {
+  background-color: #333;
+}
+.navbar-inverse .navbar-toggle .icon-bar {
+  background-color: #fff;
+}
+.navbar-inverse .navbar-collapse,
+.navbar-inverse .navbar-form {
+  border-color: #101010;
+}
+.navbar-inverse .navbar-nav > .open > a, .navbar-inverse .navbar-nav > .open > a:hover, .navbar-inverse .navbar-nav > .open > a:focus {
+  background-color: #090909;
+  color: #fff;
+}
+@media (max-width: 767px) {
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {
+    border-color: #090909;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu .divider {
+    background-color: #090909;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {
+    color: #9d9d9d;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {
+    color: #fff;
+    background-color: transparent;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {
+    color: #fff;
+    background-color: #090909;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {
+    color: #444;
+    background-color: transparent;
+  }
+}
+.navbar-inverse .navbar-link {
+  color: #9d9d9d;
+}
+.navbar-inverse .navbar-link:hover {
+  color: #fff;
+}
+.navbar-inverse .btn-link {
+  color: #9d9d9d;
+}
+.navbar-inverse .btn-link:hover, .navbar-inverse .btn-link:focus {
+  color: #fff;
+}
+.navbar-inverse .btn-link[disabled]:hover, .navbar-inverse .btn-link[disabled]:focus, fieldset[disabled] .navbar-inverse .btn-link:hover, fieldset[disabled] .navbar-inverse .btn-link:focus {
+  color: #444;
+}
+
+.breadcrumb {
+  padding: 8px 15px;
+  margin-bottom: 20px;
+  list-style: none;
+  background-color: #f5f5f5;
+  border-radius: 4px;
+}
+.breadcrumb > li {
+  display: inline-block;
+}
+.breadcrumb > li + li:before {
+  content: "/ ";
+  padding: 0 5px;
+  color: #ccc;
+}
+.breadcrumb > .active {
+  color: #777777;
+}
+
+.pagination {
+  display: inline-block;
+  padding-left: 0;
+  margin: 20px 0;
+  border-radius: 4px;
+}
+.pagination > li {
+  display: inline;
+}
+.pagination > li > a,
+.pagination > li > span {
+  position: relative;
+  float: left;
+  padding: 10px 20px;
+  line-height: 1.42857;
+  text-decoration: none;
+  color: #337ab7;
+  background-color: #fff;
+  border: 1px solid #ddd;
+  margin-left: -1px;
+}
+.pagination > li:first-child > a,
+.pagination > li:first-child > span {
+  margin-left: 0;
+  border-bottom-left-radius: 4px;
+  border-top-left-radius: 4px;
+}
+.pagination > li:last-child > a,
+.pagination > li:last-child > span {
+  border-bottom-right-radius: 4px;
+  border-top-right-radius: 4px;
+}
+.pagination > li > a:hover, .pagination > li > a:focus,
+.pagination > li > span:hover,
+.pagination > li > span:focus {
+  z-index: 3;
+  color: #23527c;
+  background-color: #eeeeee;
+  border-color: #ddd;
+}
+.pagination > .active > a, .pagination > .active > a:hover, .pagination > .active > a:focus,
+.pagination > .active > span,
+.pagination > .active > span:hover,
+.pagination > .active > span:focus {
+  z-index: 2;
+  color: #fff;
+  background-color: #337ab7;
+  border-color: #337ab7;
+  cursor: default;
+}
+.pagination > .disabled > span,
+.pagination > .disabled > span:hover,
+.pagination > .disabled > span:focus,
+.pagination > .disabled > a,
+.pagination > .disabled > a:hover,
+.pagination > .disabled > a:focus {
+  color: #777777;
+  background-color: #fff;
+  border-color: #ddd;
+  cursor: not-allowed;
+}
+
+.pagination-lg > li > a,
+.pagination-lg > li > span {
+  padding: 10px 20px;
+  font-size: 18px;
+  line-height: 1.33333;
+}
+.pagination-lg > li:first-child > a,
+.pagination-lg > li:first-child > span {
+  border-bottom-left-radius: 6px;
+  border-top-left-radius: 6px;
+}
+.pagination-lg > li:last-child > a,
+.pagination-lg > li:last-child > span {
+  border-bottom-right-radius: 6px;
+  border-top-right-radius: 6px;
+}
+
+.pagination-sm > li > a,
+.pagination-sm > li > span {
+  padding: 5px 20px;
+  font-size: 12px;
+  line-height: 1.5;
+}
+.pagination-sm > li:first-child > a,
+.pagination-sm > li:first-child > span {
+  border-bottom-left-radius: 3px;
+  border-top-left-radius: 3px;
+}
+.pagination-sm > li:last-child > a,
+.pagination-sm > li:last-child > span {
+  border-bottom-right-radius: 3px;
+  border-top-right-radius: 3px;
+}
+
+.pager {
+  padding-left: 0;
+  margin: 20px 0;
+  list-style: none;
+  text-align: center;
+}
+.pager:before, .pager:after {
+  content: " ";
+  display: table;
+}
+.pager:after {
+  clear: both;
+}
+.pager li {
+  display: inline;
+}
+.pager li > a,
+.pager li > span {
+  display: inline-block;
+  padding: 5px 14px;
+  background-color: #fff;
+  border: 1px solid #ddd;
+  border-radius: 15px;
+}
+.pager li > a:hover,
+.pager li > a:focus {
+  text-decoration: none;
+  background-color: #eeeeee;
+}
+.pager .next > a,
+.pager .next > span {
+  float: right;
+}
+.pager .previous > a,
+.pager .previous > span {
+  float: left;
+}
+.pager .disabled > a,
+.pager .disabled > a:hover,
+.pager .disabled > a:focus,
+.pager .disabled > span {
+  color: #777777;
+  background-color: #fff;
+  cursor: not-allowed;
+}
+
+.label {
+  display: inline;
+  padding: .2em .6em .3em;
+  font-size: 75%;
+  font-weight: bold;
+  line-height: 1;
+  color: #fff;
+  text-align: center;
+  white-space: nowrap;
+  vertical-align: baseline;
+  border-radius: .25em;
+}
+.label:empty {
+  display: none;
+}
+.btn .label {
+  position: relative;
+  top: -1px;
+}
+
+a.label:hover, a.label:focus {
+  color: #fff;
+  text-decoration: none;
+  cursor: pointer;
+}
+
+.label-default {
+  background-color: #777777;
+}
+.label-default[href]:hover, .label-default[href]:focus {
+  background-color: #5e5e5e;
+}
+
+.label-primary {
+  background-color: #337ab7;
+}
+.label-primary[href]:hover, .label-primary[href]:focus {
+  background-color: #286090;
+}
+
+.label-success {
+  background-color: #5cb85c;
+}
+.label-success[href]:hover, .label-success[href]:focus {
+  background-color: #449d44;
+}
+
+.label-info {
+  background-color: #5bc0de;
+}
+.label-info[href]:hover, .label-info[href]:focus {
+  background-color: #31b0d5;
+}
+
+.label-warning {
+  background-color: #f0ad4e;
+}
+.label-warning[href]:hover, .label-warning[href]:focus {
+  background-color: #ec971f;
+}
+
+.label-danger {
+  background-color: #d9534f;
+}
+.label-danger[href]:hover, .label-danger[href]:focus {
+  background-color: #c9302c;
+}
+
+.badge {
+  display: inline-block;
+  min-width: 10px;
+  padding: 3px 7px;
+  font-size: 12px;
+  font-weight: bold;
+  color: #fff;
+  line-height: 1;
+  vertical-align: middle;
+  white-space: nowrap;
+  text-align: center;
+  background-color: #777777;
+  border-radius: 10px;
+}
+.badge:empty {
+  display: none;
+}
+.btn .badge {
+  position: relative;
+  top: -1px;
+}
+.btn-xs .badge, .btn-group-xs > .btn .badge, .btn-group-xs > .btn .badge {
+  top: 0;
+  padding: 1px 5px;
+}
+.list-group-item.active > .badge, .nav-pills > .active > a > .badge {
+  color: #337ab7;
+  background-color: #fff;
+}
+.list-group-item > .badge {
+  float: right;
+}
+.list-group-item > .badge + .badge {
+  margin-right: 5px;
+}
+.nav-pills > li > a > .badge {
+  margin-left: 3px;
+}
+
+a.badge:hover, a.badge:focus {
+  color: #fff;
+  text-decoration: none;
+  cursor: pointer;
+}
+
+.jumbotron {
+  padding-top: 30px;
+  padding-bottom: 30px;
+  margin-bottom: 30px;
+  color: inherit;
+  background-color: #eeeeee;
+}
+.jumbotron h1,
+.jumbotron .h1 {
+  color: inherit;
+}
+.jumbotron p {
+  margin-bottom: 15px;
+  font-size: 21px;
+  font-weight: 200;
+}
+.jumbotron > hr {
+  border-top-color: #d5d5d5;
+}
+.container .jumbotron, .container-fluid .jumbotron {
+  border-radius: 6px;
+}
+.jumbotron .container {
+  max-width: 100%;
+}
+@media screen and (min-width: 768px) {
+  .jumbotron {
+    padding-top: 48px;
+    padding-bottom: 48px;
+  }
+  .container .jumbotron, .container-fluid .jumbotron {
+    padding-left: 60px;
+    padding-right: 60px;
+  }
+  .jumbotron h1,
+  .jumbotron .h1 {
+    font-size: 63px;
+  }
+}
+
+.thumbnail {
+  display: block;
+  padding: 4px;
+  margin-bottom: 20px;
+  line-height: 1.42857;
+  background-color: #fff;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  -webkit-transition: border 0.2s ease-in-out;
+  -o-transition: border 0.2s ease-in-out;
+  transition: border 0.2s ease-in-out;
+}
+.thumbnail > img,
+.thumbnail a > img {
+  display: block;
+  max-width: 100%;
+  height: auto;
+  margin-left: auto;
+  margin-right: auto;
+}
+.thumbnail .caption {
+  padding: 9px;
+  color: #333333;
+}
+
+a.thumbnail:hover,
+a.thumbnail:focus,
+a.thumbnail.active {
+  border-color: #337ab7;
+}
+
+.alert {
+  padding: 15px;
+  margin-bottom: 20px;
+  border: 1px solid transparent;
+  border-radius: 4px;
+}
+.alert h4 {
+  margin-top: 0;
+  color: inherit;
+}
+.alert .alert-link {
+  font-weight: bold;
+}
+.alert > p,
+.alert > ul {
+  margin-bottom: 0;
+}
+.alert > p + p {
+  margin-top: 5px;
+}
+
+.alert-dismissable,
+.alert-dismissible {
+  padding-right: 35px;
+}
+.alert-dismissable .close,
+.alert-dismissible .close {
+  position: relative;
+  top: -2px;
+  right: -21px;
+  color: inherit;
+}
+
+.alert-success {
+  background-color: #dff0d8;
+  border-color: #d6e9c6;
+  color: #3c763d;
+}
+.alert-success hr {
+  border-top-color: #c9e2b3;
+}
+.alert-success .alert-link {
+  color: #2b542c;
+}
+
+.alert-info {
+  background-color: #d9edf7;
+  border-color: #bce8f1;
+  color: #31708f;
+}
+.alert-info hr {
+  border-top-color: #a6e1ec;
+}
+.alert-info .alert-link {
+  color: #245269;
+}
+
+.alert-warning {
+  background-color: #fcf8e3;
+  border-color: #faebcc;
+  color: #8a6d3b;
+}
+.alert-warning hr {
+  border-top-color: #f7e1b5;
+}
+.alert-warning .alert-link {
+  color: #66512c;
+}
+
+.alert-danger {
+  background-color: #f2dede;
+  border-color: #ebccd1;
+  color: #a94442;
+}
+.alert-danger hr {
+  border-top-color: #e4b9c0;
+}
+.alert-danger .alert-link {
+  color: #843534;
+}
+
+@-webkit-keyframes progress-bar-stripes {
+  from {
+    background-position: 40px 0;
+  }
+  to {
+    background-position: 0 0;
+  }
+}
+@keyframes progress-bar-stripes {
+  from {
+    background-position: 40px 0;
+  }
+  to {
+    background-position: 0 0;
+  }
+}
+.progress {
+  overflow: hidden;
+  height: 20px;
+  margin-bottom: 20px;
+  background-color: #f5f5f5;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+}
+
+.progress-bar {
+  float: left;
+  width: 0%;
+  height: 100%;
+  font-size: 12px;
+  line-height: 20px;
+  color: #fff;
+  text-align: center;
+  background-color: #337ab7;
+  -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+  -webkit-transition: width 0.6s ease;
+  -o-transition: width 0.6s ease;
+  transition: width 0.6s ease;
+}
+
+.progress-striped .progress-bar,
+.progress-bar-striped {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-size: 40px 40px;
+}
+
+.progress.active .progress-bar,
+.progress-bar.active {
+  -webkit-animation: progress-bar-stripes 2s linear infinite;
+  -o-animation: progress-bar-stripes 2s linear infinite;
+  animation: progress-bar-stripes 2s linear infinite;
+}
+
+.progress-bar-success {
+  background-color: #5cb85c;
+}
+.progress-striped .progress-bar-success {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+}
+
+.progress-bar-info {
+  background-color: #5bc0de;
+}
+.progress-striped .progress-bar-info {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+}
+
+.progress-bar-warning {
+  background-color: #f0ad4e;
+}
+.progress-striped .progress-bar-warning {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+}
+
+.progress-bar-danger {
+  background-color: #d9534f;
+}
+.progress-striped .progress-bar-danger {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+}
+
+.media {
+  margin-top: 15px;
+}
+.media:first-child {
+  margin-top: 0;
+}
+
+.media,
+.media-body {
+  zoom: 1;
+  overflow: hidden;
+}
+
+.media-body {
+  width: 10000px;
+}
+
+.media-object {
+  display: block;
+}
+.media-object.img-thumbnail {
+  max-width: none;
+}
+
+.media-right,
+.media > .pull-right {
+  padding-left: 10px;
+}
+
+.media-left,
+.media > .pull-left {
+  padding-right: 10px;
+}
+
+.media-left,
+.media-right,
+.media-body {
+  display: table-cell;
+  vertical-align: top;
+}
+
+.media-middle {
+  vertical-align: middle;
+}
+
+.media-bottom {
+  vertical-align: bottom;
+}
+
+.media-heading {
+  margin-top: 0;
+  margin-bottom: 5px;
+}
+
+.media-list {
+  padding-left: 0;
+  list-style: none;
+}
+
+.list-group {
+  margin-bottom: 20px;
+  padding-left: 0;
+}
+
+.list-group-item {
+  position: relative;
+  display: block;
+  padding: 10px 15px;
+  margin-bottom: -1px;
+  background-color: #fff;
+  border: 1px solid #ddd;
+}
+.list-group-item:first-child {
+  border-top-right-radius: 4px;
+  border-top-left-radius: 4px;
+}
+.list-group-item:last-child {
+  margin-bottom: 0;
+  border-bottom-right-radius: 4px;
+  border-bottom-left-radius: 4px;
+}
+
+a.list-group-item,
+button.list-group-item {
+  color: #555;
+}
+a.list-group-item .list-group-item-heading,
+button.list-group-item .list-group-item-heading {
+  color: #333;
+}
+a.list-group-item:hover, a.list-group-item:focus,
+button.list-group-item:hover,
+button.list-group-item:focus {
+  text-decoration: none;
+  color: #555;
+  background-color: #f5f5f5;
+}
+
+button.list-group-item {
+  width: 100%;
+  text-align: left;
+}
+
+.list-group-item.disabled, .list-group-item.disabled:hover, .list-group-item.disabled:focus {
+  background-color: #eeeeee;
+  color: #777777;
+  cursor: not-allowed;
+}
+.list-group-item.disabled .list-group-item-heading, .list-group-item.disabled:hover .list-group-item-heading, .list-group-item.disabled:focus .list-group-item-heading {
+  color: inherit;
+}
+.list-group-item.disabled .list-group-item-text, .list-group-item.disabled:hover .list-group-item-text, .list-group-item.disabled:focus .list-group-item-text {
+  color: #777777;
+}
+.list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus {
+  z-index: 2;
+  color: #fff;
+  background-color: #337ab7;
+  border-color: #337ab7;
+}
+.list-group-item.active .list-group-item-heading,
+.list-group-item.active .list-group-item-heading > small,
+.list-group-item.active .list-group-item-heading > .small, .list-group-item.active:hover .list-group-item-heading,
+.list-group-item.active:hover .list-group-item-heading > small,
+.list-group-item.active:hover .list-group-item-heading > .small, .list-group-item.active:focus .list-group-item-heading,
+.list-group-item.active:focus .list-group-item-heading > small,
+.list-group-item.active:focus .list-group-item-heading > .small {
+  color: inherit;
+}
+.list-group-item.active .list-group-item-text, .list-group-item.active:hover .list-group-item-text, .list-group-item.active:focus .list-group-item-text {
+  color: #c7ddef;
+}
+
+.list-group-item-success {
+  color: #3c763d;
+  background-color: #dff0d8;
+}
+
+a.list-group-item-success,
+button.list-group-item-success {
+  color: #3c763d;
+}
+a.list-group-item-success .list-group-item-heading,
+button.list-group-item-success .list-group-item-heading {
+  color: inherit;
+}
+a.list-group-item-success:hover, a.list-group-item-success:focus,
+button.list-group-item-success:hover,
+button.list-group-item-success:focus {
+  color: #3c763d;
+  background-color: #d0e9c6;
+}
+a.list-group-item-success.active, a.list-group-item-success.active:hover, a.list-group-item-success.active:focus,
+button.list-group-item-success.active,
+button.list-group-item-success.active:hover,
+button.list-group-item-success.active:focus {
+  color: #fff;
+  background-color: #3c763d;
+  border-color: #3c763d;
+}
+
+.list-group-item-info {
+  color: #31708f;
+  background-color: #d9edf7;
+}
+
+a.list-group-item-info,
+button.list-group-item-info {
+  color: #31708f;
+}
+a.list-group-item-info .list-group-item-heading,
+button.list-group-item-info .list-group-item-heading {
+  color: inherit;
+}
+a.list-group-item-info:hover, a.list-group-item-info:focus,
+button.list-group-item-info:hover,
+button.list-group-item-info:focus {
+  color: #31708f;
+  background-color: #c4e3f3;
+}
+a.list-group-item-info.active, a.list-group-item-info.active:hover, a.list-group-item-info.active:focus,
+button.list-group-item-info.active,
+button.list-group-item-info.active:hover,
+button.list-group-item-info.active:focus {
+  color: #fff;
+  background-color: #31708f;
+  border-color: #31708f;
+}
+
+.list-group-item-warning {
+  color: #8a6d3b;
+  background-color: #fcf8e3;
+}
+
+a.list-group-item-warning,
+button.list-group-item-warning {
+  color: #8a6d3b;
+}
+a.list-group-item-warning .list-group-item-heading,
+button.list-group-item-warning .list-group-item-heading {
+  color: inherit;
+}
+a.list-group-item-warning:hover, a.list-group-item-warning:focus,
+button.list-group-item-warning:hover,
+button.list-group-item-warning:focus {
+  color: #8a6d3b;
+  background-color: #faf2cc;
+}
+a.list-group-item-warning.active, a.list-group-item-warning.active:hover, a.list-group-item-warning.active:focus,
+button.list-group-item-warning.active,
+button.list-group-item-warning.active:hover,
+button.list-group-item-warning.active:focus {
+  color: #fff;
+  background-color: #8a6d3b;
+  border-color: #8a6d3b;
+}
+
+.list-group-item-danger {
+  color: #a94442;
+  background-color: #f2dede;
+}
+
+a.list-group-item-danger,
+button.list-group-item-danger {
+  color: #a94442;
+}
+a.list-group-item-danger .list-group-item-heading,
+button.list-group-item-danger .list-group-item-heading {
+  color: inherit;
+}
+a.list-group-item-danger:hover, a.list-group-item-danger:focus,
+button.list-group-item-danger:hover,
+button.list-group-item-danger:focus {
+  color: #a94442;
+  background-color: #ebcccc;
+}
+a.list-group-item-danger.active, a.list-group-item-danger.active:hover, a.list-group-item-danger.active:focus,
+button.list-group-item-danger.active,
+button.list-group-item-danger.active:hover,
+button.list-group-item-danger.active:focus {
+  color: #fff;
+  background-color: #a94442;
+  border-color: #a94442;
+}
+
+.list-group-item-heading {
+  margin-top: 0;
+  margin-bottom: 5px;
+}
+
+.list-group-item-text {
+  margin-bottom: 0;
+  line-height: 1.3;
+}
+
+.panel {
+  margin-bottom: 20px;
+  background-color: #fff;
+  border: 1px solid transparent;
+  border-radius: 4px;
+  -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
+  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
+}
+
+.panel-body {
+  padding: 15px;
+}
+.panel-body:before, .panel-body:after {
+  content: " ";
+  display: table;
+}
+.panel-body:after {
+  clear: both;
+}
+
+.panel-heading {
+  padding: 10px 15px;
+  border-bottom: 1px solid transparent;
+  border-top-right-radius: 3px;
+  border-top-left-radius: 3px;
+}
+.panel-heading > .dropdown .dropdown-toggle {
+  color: inherit;
+}
+
+.panel-title {
+  margin-top: 0;
+  margin-bottom: 0;
+  font-size: 16px;
+  color: inherit;
+}
+.panel-title > a,
+.panel-title > small,
+.panel-title > .small,
+.panel-title > small > a,
+.panel-title > .small > a {
+  color: inherit;
+}
+
+.panel-footer {
+  padding: 10px 15px;
+  background-color: #f5f5f5;
+  border-top: 1px solid #ddd;
+  border-bottom-right-radius: 3px;
+  border-bottom-left-radius: 3px;
+}
+
+.panel > .list-group,
+.panel > .panel-collapse > .list-group {
+  margin-bottom: 0;
+}
+.panel > .list-group .list-group-item,
+.panel > .panel-collapse > .list-group .list-group-item {
+  border-width: 1px 0;
+  border-radius: 0;
+}
+.panel > .list-group:first-child .list-group-item:first-child,
+.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {
+  border-top: 0;
+  border-top-right-radius: 3px;
+  border-top-left-radius: 3px;
+}
+.panel > .list-group:last-child .list-group-item:last-child,
+.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {
+  border-bottom: 0;
+  border-bottom-right-radius: 3px;
+  border-bottom-left-radius: 3px;
+}
+.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child {
+  border-top-right-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.panel-heading + .list-group .list-group-item:first-child {
+  border-top-width: 0;
+}
+
+.list-group + .panel-footer {
+  border-top-width: 0;
+}
+
+.panel > .table,
+.panel > .table-responsive > .table,
+.panel > .panel-collapse > .table {
+  margin-bottom: 0;
+}
+.panel > .table caption,
+.panel > .table-responsive > .table caption,
+.panel > .panel-collapse > .table caption {
+  padding-left: 15px;
+  padding-right: 15px;
+}
+.panel > .table:first-child,
+.panel > .table-responsive:first-child > .table:first-child {
+  border-top-right-radius: 3px;
+  border-top-left-radius: 3px;
+}
+.panel > .table:first-child > thead:first-child > tr:first-child,
+.panel > .table:first-child > tbody:first-child > tr:first-child,
+.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,
+.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {
+  border-top-left-radius: 3px;
+  border-top-right-radius: 3px;
+}
+.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,
+.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,
+.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,
+.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,
+.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,
+.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,
+.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,
+.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {
+  border-top-left-radius: 3px;
+}
+.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,
+.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,
+.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,
+.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,
+.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,
+.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,
+.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,
+.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {
+  border-top-right-radius: 3px;
+}
+.panel > .table:last-child,
+.panel > .table-responsive:last-child > .table:last-child {
+  border-bottom-right-radius: 3px;
+  border-bottom-left-radius: 3px;
+}
+.panel > .table:last-child > tbody:last-child > tr:last-child,
+.panel > .table:last-child > tfoot:last-child > tr:last-child,
+.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,
+.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {
+  border-bottom-left-radius: 3px;
+  border-bottom-right-radius: 3px;
+}
+.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,
+.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,
+.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,
+.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,
+.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,
+.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,
+.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,
+.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {
+  border-bottom-left-radius: 3px;
+}
+.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,
+.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,
+.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,
+.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,
+.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,
+.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,
+.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,
+.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {
+  border-bottom-right-radius: 3px;
+}
+.panel > .panel-body + .table,
+.panel > .panel-body + .table-responsive,
+.panel > .table + .panel-body,
+.panel > .table-responsive + .panel-body {
+  border-top: 1px solid #ddd;
+}
+.panel > .table > tbody:first-child > tr:first-child th,
+.panel > .table > tbody:first-child > tr:first-child td {
+  border-top: 0;
+}
+.panel > .table-bordered,
+.panel > .table-responsive > .table-bordered {
+  border: 0;
+}
+.panel > .table-bordered > thead > tr > th:first-child,
+.panel > .table-bordered > thead > tr > td:first-child,
+.panel > .table-bordered > tbody > tr > th:first-child,
+.panel > .table-bordered > tbody > tr > td:first-child,
+.panel > .table-bordered > tfoot > tr > th:first-child,
+.panel > .table-bordered > tfoot > tr > td:first-child,
+.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,
+.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,
+.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,
+.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,
+.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,
+.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {
+  border-left: 0;
+}
+.panel > .table-bordered > thead > tr > th:last-child,
+.panel > .table-bordered > thead > tr > td:last-child,
+.panel > .table-bordered > tbody > tr > th:last-child,
+.panel > .table-bordered > tbody > tr > td:last-child,
+.panel > .table-bordered > tfoot > tr > th:last-child,
+.panel > .table-bordered > tfoot > tr > td:last-child,
+.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,
+.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,
+.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,
+.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,
+.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,
+.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {
+  border-right: 0;
+}
+.panel > .table-bordered > thead > tr:first-child > td,
+.panel > .table-bordered > thead > tr:first-child > th,
+.panel > .table-bordered > tbody > tr:first-child > td,
+.panel > .table-bordered > tbody > tr:first-child > th,
+.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,
+.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,
+.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,
+.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {
+  border-bottom: 0;
+}
+.panel > .table-bordered > tbody > tr:last-child > td,
+.panel > .table-bordered > tbody > tr:last-child > th,
+.panel > .table-bordered > tfoot > tr:last-child > td,
+.panel > .table-bordered > tfoot > tr:last-child > th,
+.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,
+.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,
+.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,
+.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {
+  border-bottom: 0;
+}
+.panel > .table-responsive {
+  border: 0;
+  margin-bottom: 0;
+}
+
+.panel-group {
+  margin-bottom: 20px;
+}
+.panel-group .panel {
+  margin-bottom: 0;
+  border-radius: 4px;
+}
+.panel-group .panel + .panel {
+  margin-top: 5px;
+}
+.panel-group .panel-heading {
+  border-bottom: 0;
+}
+.panel-group .panel-heading + .panel-collapse > .panel-body,
+.panel-group .panel-heading + .panel-collapse > .list-group {
+  border-top: 1px solid #ddd;
+}
+.panel-group .panel-footer {
+  border-top: 0;
+}
+.panel-group .panel-footer + .panel-collapse .panel-body {
+  border-bottom: 1px solid #ddd;
+}
+
+.panel-default {
+  border-color: #ddd;
+}
+.panel-default > .panel-heading {
+  color: #333333;
+  background-color: #f5f5f5;
+  border-color: #ddd;
+}
+.panel-default > .panel-heading + .panel-collapse > .panel-body {
+  border-top-color: #ddd;
+}
+.panel-default > .panel-heading .badge {
+  color: #f5f5f5;
+  background-color: #333333;
+}
+.panel-default > .panel-footer + .panel-collapse > .panel-body {
+  border-bottom-color: #ddd;
+}
+
+.panel-primary {
+  border-color: #337ab7;
+}
+.panel-primary > .panel-heading {
+  color: #fff;
+  background-color: #337ab7;
+  border-color: #337ab7;
+}
+.panel-primary > .panel-heading + .panel-collapse > .panel-body {
+  border-top-color: #337ab7;
+}
+.panel-primary > .panel-heading .badge {
+  color: #337ab7;
+  background-color: #fff;
+}
+.panel-primary > .panel-footer + .panel-collapse > .panel-body {
+  border-bottom-color: #337ab7;
+}
+
+.panel-success {
+  border-color: #d6e9c6;
+}
+.panel-success > .panel-heading {
+  color: #3c763d;
+  background-color: #dff0d8;
+  border-color: #d6e9c6;
+}
+.panel-success > .panel-heading + .panel-collapse > .panel-body {
+  border-top-color: #d6e9c6;
+}
+.panel-success > .panel-heading .badge {
+  color: #dff0d8;
+  background-color: #3c763d;
+}
+.panel-success > .panel-footer + .panel-collapse > .panel-body {
+  border-bottom-color: #d6e9c6;
+}
+
+.panel-info {
+  border-color: #bce8f1;
+}
+.panel-info > .panel-heading {
+  color: #31708f;
+  background-color: #d9edf7;
+  border-color: #bce8f1;
+}
+.panel-info > .panel-heading + .panel-collapse > .panel-body {
+  border-top-color: #bce8f1;
+}
+.panel-info > .panel-heading .badge {
+  color: #d9edf7;
+  background-color: #31708f;
+}
+.panel-info > .panel-footer + .panel-collapse > .panel-body {
+  border-bottom-color: #bce8f1;
+}
+
+.panel-warning {
+  border-color: #faebcc;
+}
+.panel-warning > .panel-heading {
+  color: #8a6d3b;
+  background-color: #fcf8e3;
+  border-color: #faebcc;
+}
+.panel-warning > .panel-heading + .panel-collapse > .panel-body {
+  border-top-color: #faebcc;
+}
+.panel-warning > .panel-heading .badge {
+  color: #fcf8e3;
+  background-color: #8a6d3b;
+}
+.panel-warning > .panel-footer + .panel-collapse > .panel-body {
+  border-bottom-color: #faebcc;
+}
+
+.panel-danger {
+  border-color: #ebccd1;
+}
+.panel-danger > .panel-heading {
+  color: #a94442;
+  background-color: #f2dede;
+  border-color: #ebccd1;
+}
+.panel-danger > .panel-heading + .panel-collapse > .panel-body {
+  border-top-color: #ebccd1;
+}
+.panel-danger > .panel-heading .badge {
+  color: #f2dede;
+  background-color: #a94442;
+}
+.panel-danger > .panel-footer + .panel-collapse > .panel-body {
+  border-bottom-color: #ebccd1;
+}
+
+.embed-responsive {
+  position: relative;
+  display: block;
+  height: 0;
+  padding: 0;
+  overflow: hidden;
+}
+.embed-responsive .embed-responsive-item,
+.embed-responsive iframe,
+.embed-responsive embed,
+.embed-responsive object,
+.embed-responsive video {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  height: 100%;
+  width: 100%;
+  border: 0;
+}
+
+.embed-responsive-16by9 {
+  padding-bottom: 56.25%;
+}
+
+.embed-responsive-4by3 {
+  padding-bottom: 75%;
+}
+
+.well {
+  min-height: 20px;
+  padding: 19px;
+  margin-bottom: 20px;
+  background-color: #f5f5f5;
+  border: 1px solid #e3e3e3;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
+}
+.well blockquote {
+  border-color: #ddd;
+  border-color: rgba(0, 0, 0, 0.15);
+}
+
+.well-lg {
+  padding: 24px;
+  border-radius: 6px;
+}
+
+.well-sm {
+  padding: 9px;
+  border-radius: 3px;
+}
+
+.close {
+  float: right;
+  font-size: 21px;
+  font-weight: bold;
+  line-height: 1;
+  color: #000;
+  text-shadow: 0 1px 0 #fff;
+  opacity: 0.2;
+  filter: alpha(opacity=20);
+}
+.close:hover, .close:focus {
+  color: #000;
+  text-decoration: none;
+  cursor: pointer;
+  opacity: 0.5;
+  filter: alpha(opacity=50);
+}
+
+button.close {
+  padding: 0;
+  cursor: pointer;
+  background: transparent;
+  border: 0;
+  -webkit-appearance: none;
+}
+
+.modal-open {
+  overflow: hidden;
+}
+
+.modal {
+  display: none;
+  overflow: hidden;
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 1050;
+  -webkit-overflow-scrolling: touch;
+  outline: 0;
+}
+.modal.fade .modal-dialog {
+  -webkit-transform: translate(0, -25%);
+  -ms-transform: translate(0, -25%);
+  -o-transform: translate(0, -25%);
+  transform: translate(0, -25%);
+  -webkit-transition: -webkit-transform 0.3s ease-out;
+  -moz-transition: -moz-transform 0.3s ease-out;
+  -o-transition: -o-transform 0.3s ease-out;
+  transition: transform 0.3s ease-out;
+}
+.modal.in .modal-dialog {
+  -webkit-transform: translate(0, 0);
+  -ms-transform: translate(0, 0);
+  -o-transform: translate(0, 0);
+  transform: translate(0, 0);
+}
+
+.modal-open .modal {
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.modal-dialog {
+  position: relative;
+  width: auto;
+  margin: 10px;
+}
+
+.modal-content {
+  position: relative;
+  background-color: #fff;
+  border: 1px solid #999;
+  border: 1px solid rgba(0, 0, 0, 0.2);
+  border-radius: 6px;
+  -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);
+  box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);
+  background-clip: padding-box;
+  outline: 0;
+}
+
+.modal-backdrop {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 1040;
+  background-color: #000;
+}
+.modal-backdrop.fade {
+  opacity: 0;
+  filter: alpha(opacity=0);
+}
+.modal-backdrop.in {
+  opacity: 0.5;
+  filter: alpha(opacity=50);
+}
+
+.modal-header {
+  padding: 15px;
+  border-bottom: 1px solid #e5e5e5;
+  min-height: 16.42857px;
+}
+
+.modal-header .close {
+  margin-top: -2px;
+}
+
+.modal-title {
+  margin: 0;
+  line-height: 1.42857;
+}
+
+.modal-body {
+  position: relative;
+  padding: 15px;
+}
+
+.modal-footer {
+  padding: 15px;
+  text-align: right;
+  border-top: 1px solid #e5e5e5;
+}
+.modal-footer:before, .modal-footer:after {
+  content: " ";
+  display: table;
+}
+.modal-footer:after {
+  clear: both;
+}
+.modal-footer .btn + .btn {
+  margin-left: 5px;
+  margin-bottom: 0;
+}
+.modal-footer .btn-group .btn + .btn {
+  margin-left: -1px;
+}
+.modal-footer .btn-block + .btn-block {
+  margin-left: 0;
+}
+
+.modal-scrollbar-measure {
+  position: absolute;
+  top: -9999px;
+  width: 50px;
+  height: 50px;
+  overflow: scroll;
+}
+
+@media (min-width: 768px) {
+  .modal-dialog {
+    width: 600px;
+    margin: 30px auto;
+  }
+
+  .modal-content {
+    -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
+    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
+  }
+
+  .modal-sm {
+    width: 300px;
+  }
+}
+@media (min-width: 992px) {
+  .modal-lg {
+    width: 900px;
+  }
+}
+.tooltip {
+  position: absolute;
+  z-index: 1070;
+  display: block;
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-style: normal;
+  font-weight: normal;
+  letter-spacing: normal;
+  line-break: auto;
+  line-height: 1.42857;
+  text-align: left;
+  text-align: start;
+  text-decoration: none;
+  text-shadow: none;
+  text-transform: none;
+  white-space: normal;
+  word-break: normal;
+  word-spacing: normal;
+  word-wrap: normal;
+  font-size: 12px;
+  opacity: 0;
+  filter: alpha(opacity=0);
+}
+.tooltip.in {
+  opacity: 0.9;
+  filter: alpha(opacity=90);
+}
+.tooltip.top {
+  margin-top: -3px;
+  padding: 5px 0;
+}
+.tooltip.right {
+  margin-left: 3px;
+  padding: 0 5px;
+}
+.tooltip.bottom {
+  margin-top: 3px;
+  padding: 5px 0;
+}
+.tooltip.left {
+  margin-left: -3px;
+  padding: 0 5px;
+}
+
+.tooltip-inner {
+  max-width: 200px;
+  padding: 3px 8px;
+  color: #fff;
+  text-align: center;
+  background-color: #000;
+  border-radius: 4px;
+}
+
+.tooltip-arrow {
+  position: absolute;
+  width: 0;
+  height: 0;
+  border-color: transparent;
+  border-style: solid;
+}
+
+.tooltip.top .tooltip-arrow {
+  bottom: 0;
+  left: 50%;
+  margin-left: -5px;
+  border-width: 5px 5px 0;
+  border-top-color: #000;
+}
+.tooltip.top-left .tooltip-arrow {
+  bottom: 0;
+  right: 5px;
+  margin-bottom: -5px;
+  border-width: 5px 5px 0;
+  border-top-color: #000;
+}
+.tooltip.top-right .tooltip-arrow {
+  bottom: 0;
+  left: 5px;
+  margin-bottom: -5px;
+  border-width: 5px 5px 0;
+  border-top-color: #000;
+}
+.tooltip.right .tooltip-arrow {
+  top: 50%;
+  left: 0;
+  margin-top: -5px;
+  border-width: 5px 5px 5px 0;
+  border-right-color: #000;
+}
+.tooltip.left .tooltip-arrow {
+  top: 50%;
+  right: 0;
+  margin-top: -5px;
+  border-width: 5px 0 5px 5px;
+  border-left-color: #000;
+}
+.tooltip.bottom .tooltip-arrow {
+  top: 0;
+  left: 50%;
+  margin-left: -5px;
+  border-width: 0 5px 5px;
+  border-bottom-color: #000;
+}
+.tooltip.bottom-left .tooltip-arrow {
+  top: 0;
+  right: 5px;
+  margin-top: -5px;
+  border-width: 0 5px 5px;
+  border-bottom-color: #000;
+}
+.tooltip.bottom-right .tooltip-arrow {
+  top: 0;
+  left: 5px;
+  margin-top: -5px;
+  border-width: 0 5px 5px;
+  border-bottom-color: #000;
+}
+
+.popover {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 1060;
+  display: none;
+  max-width: 276px;
+  padding: 1px;
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-style: normal;
+  font-weight: normal;
+  letter-spacing: normal;
+  line-break: auto;
+  line-height: 1.42857;
+  text-align: left;
+  text-align: start;
+  text-decoration: none;
+  text-shadow: none;
+  text-transform: none;
+  white-space: normal;
+  word-break: normal;
+  word-spacing: normal;
+  word-wrap: normal;
+  font-size: 14px;
+  background-color: #fff;
+  background-clip: padding-box;
+  border: 1px solid #ccc;
+  border: 1px solid rgba(0, 0, 0, 0.2);
+  border-radius: 6px;
+  -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+}
+.popover.top {
+  margin-top: -10px;
+}
+.popover.right {
+  margin-left: 10px;
+}
+.popover.bottom {
+  margin-top: 10px;
+}
+.popover.left {
+  margin-left: -10px;
+}
+
+.popover-title {
+  margin: 0;
+  padding: 8px 14px;
+  font-size: 14px;
+  background-color: #f7f7f7;
+  border-bottom: 1px solid #ebebeb;
+  border-radius: 5px 5px 0 0;
+}
+
+.popover-content {
+  padding: 9px 14px;
+}
+
+.popover > .arrow, .popover > .arrow:after {
+  position: absolute;
+  display: block;
+  width: 0;
+  height: 0;
+  border-color: transparent;
+  border-style: solid;
+}
+
+.popover > .arrow {
+  border-width: 11px;
+}
+
+.popover > .arrow:after {
+  border-width: 10px;
+  content: "";
+}
+
+.popover.top > .arrow {
+  left: 50%;
+  margin-left: -11px;
+  border-bottom-width: 0;
+  border-top-color: #999999;
+  border-top-color: rgba(0, 0, 0, 0.25);
+  bottom: -11px;
+}
+.popover.top > .arrow:after {
+  content: " ";
+  bottom: 1px;
+  margin-left: -10px;
+  border-bottom-width: 0;
+  border-top-color: #fff;
+}
+.popover.right > .arrow {
+  top: 50%;
+  left: -11px;
+  margin-top: -11px;
+  border-left-width: 0;
+  border-right-color: #999999;
+  border-right-color: rgba(0, 0, 0, 0.25);
+}
+.popover.right > .arrow:after {
+  content: " ";
+  left: 1px;
+  bottom: -10px;
+  border-left-width: 0;
+  border-right-color: #fff;
+}
+.popover.bottom > .arrow {
+  left: 50%;
+  margin-left: -11px;
+  border-top-width: 0;
+  border-bottom-color: #999999;
+  border-bottom-color: rgba(0, 0, 0, 0.25);
+  top: -11px;
+}
+.popover.bottom > .arrow:after {
+  content: " ";
+  top: 1px;
+  margin-left: -10px;
+  border-top-width: 0;
+  border-bottom-color: #fff;
+}
+.popover.left > .arrow {
+  top: 50%;
+  right: -11px;
+  margin-top: -11px;
+  border-right-width: 0;
+  border-left-color: #999999;
+  border-left-color: rgba(0, 0, 0, 0.25);
+}
+.popover.left > .arrow:after {
+  content: " ";
+  right: 1px;
+  border-right-width: 0;
+  border-left-color: #fff;
+  bottom: -10px;
+}
+
+.carousel {
+  position: relative;
+}
+
+.carousel-inner {
+  position: relative;
+  overflow: hidden;
+  width: 100%;
+}
+.carousel-inner > .item {
+  display: none;
+  position: relative;
+  -webkit-transition: 0.6s ease-in-out left;
+  -o-transition: 0.6s ease-in-out left;
+  transition: 0.6s ease-in-out left;
+}
+.carousel-inner > .item > img,
+.carousel-inner > .item > a > img {
+  display: block;
+  max-width: 100%;
+  height: auto;
+  line-height: 1;
+}
+@media all and (transform-3d), (-webkit-transform-3d) {
+  .carousel-inner > .item {
+    -webkit-transition: -webkit-transform 0.6s ease-in-out;
+    -moz-transition: -moz-transform 0.6s ease-in-out;
+    -o-transition: -o-transform 0.6s ease-in-out;
+    transition: transform 0.6s ease-in-out;
+    -webkit-backface-visibility: hidden;
+    -moz-backface-visibility: hidden;
+    backface-visibility: hidden;
+    -webkit-perspective: 1000px;
+    -moz-perspective: 1000px;
+    perspective: 1000px;
+  }
+  .carousel-inner > .item.next, .carousel-inner > .item.active.right {
+    -webkit-transform: translate3d(100%, 0, 0);
+    transform: translate3d(100%, 0, 0);
+    left: 0;
+  }
+  .carousel-inner > .item.prev, .carousel-inner > .item.active.left {
+    -webkit-transform: translate3d(-100%, 0, 0);
+    transform: translate3d(-100%, 0, 0);
+    left: 0;
+  }
+  .carousel-inner > .item.next.left, .carousel-inner > .item.prev.right, .carousel-inner > .item.active {
+    -webkit-transform: translate3d(0, 0, 0);
+    transform: translate3d(0, 0, 0);
+    left: 0;
+  }
+}
+.carousel-inner > .active,
+.carousel-inner > .next,
+.carousel-inner > .prev {
+  display: block;
+}
+.carousel-inner > .active {
+  left: 0;
+}
+.carousel-inner > .next,
+.carousel-inner > .prev {
+  position: absolute;
+  top: 0;
+  width: 100%;
+}
+.carousel-inner > .next {
+  left: 100%;
+}
+.carousel-inner > .prev {
+  left: -100%;
+}
+.carousel-inner > .next.left,
+.carousel-inner > .prev.right {
+  left: 0;
+}
+.carousel-inner > .active.left {
+  left: -100%;
+}
+.carousel-inner > .active.right {
+  left: 100%;
+}
+
+.carousel-control {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  width: 15%;
+  opacity: 0.5;
+  filter: alpha(opacity=50);
+  font-size: 20px;
+  color: #fff;
+  text-align: center;
+  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
+}
+.carousel-control.left {
+  background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);
+  background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);
+  background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);
+}
+.carousel-control.right {
+  left: auto;
+  right: 0;
+  background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);
+  background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);
+  background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);
+}
+.carousel-control:hover, .carousel-control:focus {
+  outline: 0;
+  color: #fff;
+  text-decoration: none;
+  opacity: 0.9;
+  filter: alpha(opacity=90);
+}
+.carousel-control .icon-prev,
+.carousel-control .icon-next,
+.carousel-control .glyphicon-chevron-left,
+.carousel-control .glyphicon-chevron-right {
+  position: absolute;
+  top: 50%;
+  margin-top: -10px;
+  z-index: 5;
+  display: inline-block;
+}
+.carousel-control .icon-prev,
+.carousel-control .glyphicon-chevron-left {
+  left: 50%;
+  margin-left: -10px;
+}
+.carousel-control .icon-next,
+.carousel-control .glyphicon-chevron-right {
+  right: 50%;
+  margin-right: -10px;
+}
+.carousel-control .icon-prev,
+.carousel-control .icon-next {
+  width: 20px;
+  height: 20px;
+  line-height: 1;
+  font-family: serif;
+}
+.carousel-control .icon-prev:before {
+  content: '\2039';
+}
+.carousel-control .icon-next:before {
+  content: '\203a';
+}
+
+.carousel-indicators {
+  position: absolute;
+  bottom: 10px;
+  left: 50%;
+  z-index: 15;
+  width: 60%;
+  margin-left: -30%;
+  padding-left: 0;
+  list-style: none;
+  text-align: center;
+}
+.carousel-indicators li {
+  display: inline-block;
+  width: 10px;
+  height: 10px;
+  margin: 1px;
+  text-indent: -999px;
+  border: 1px solid #fff;
+  border-radius: 10px;
+  cursor: pointer;
+  background-color: #000 \9;
+  background-color: transparent;
+}
+.carousel-indicators .active {
+  margin: 0;
+  width: 12px;
+  height: 12px;
+  background-color: #fff;
+}
+
+.carousel-caption {
+  position: absolute;
+  left: 15%;
+  right: 15%;
+  bottom: 20px;
+  z-index: 10;
+  padding-top: 20px;
+  padding-bottom: 20px;
+  color: #fff;
+  text-align: center;
+  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
+}
+.carousel-caption .btn {
+  text-shadow: none;
+}
+
+@media screen and (min-width: 768px) {
+  .carousel-control .glyphicon-chevron-left,
+  .carousel-control .glyphicon-chevron-right,
+  .carousel-control .icon-prev,
+  .carousel-control .icon-next {
+    width: 30px;
+    height: 30px;
+    margin-top: -15px;
+    font-size: 30px;
+  }
+  .carousel-control .glyphicon-chevron-left,
+  .carousel-control .icon-prev {
+    margin-left: -15px;
+  }
+  .carousel-control .glyphicon-chevron-right,
+  .carousel-control .icon-next {
+    margin-right: -15px;
+  }
+
+  .carousel-caption {
+    left: 20%;
+    right: 20%;
+    padding-bottom: 30px;
+  }
+
+  .carousel-indicators {
+    bottom: 20px;
+  }
+}
+.clearfix:before, .clearfix:after {
+  content: " ";
+  display: table;
+}
+.clearfix:after {
+  clear: both;
+}
+
+.center-block {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.pull-right {
+  float: right !important;
+}
+
+.pull-left {
+  float: left !important;
+}
+
+.hide {
+  display: none !important;
+}
+
+.show {
+  display: block !important;
+}
+
+.invisible {
+  visibility: hidden;
+}
+
+.text-hide {
+  font: 0/0 a;
+  color: transparent;
+  text-shadow: none;
+  background-color: transparent;
+  border: 0;
+}
+
+.hidden {
+  display: none !important;
+}
+
+.affix {
+  position: fixed;
+}
+
+@-ms-viewport {
+  width: device-width;
+}
+.visible-xs {
+  display: none !important;
+}
+
+.visible-sm {
+  display: none !important;
+}
+
+.visible-md {
+  display: none !important;
+}
+
+.visible-lg {
+  display: none !important;
+}
+
+.visible-xs-block,
+.visible-xs-inline,
+.visible-xs-inline-block,
+.visible-sm-block,
+.visible-sm-inline,
+.visible-sm-inline-block,
+.visible-md-block,
+.visible-md-inline,
+.visible-md-inline-block,
+.visible-lg-block,
+.visible-lg-inline,
+.visible-lg-inline-block {
+  display: none !important;
+}
+
+@media (max-width: 767px) {
+  .visible-xs {
+    display: block !important;
+  }
+
+  table.visible-xs {
+    display: table !important;
+  }
+
+  tr.visible-xs {
+    display: table-row !important;
+  }
+
+  th.visible-xs,
+  td.visible-xs {
+    display: table-cell !important;
+  }
+}
+@media (max-width: 767px) {
+  .visible-xs-block {
+    display: block !important;
+  }
+}
+
+@media (max-width: 767px) {
+  .visible-xs-inline {
+    display: inline !important;
+  }
+}
+
+@media (max-width: 767px) {
+  .visible-xs-inline-block {
+    display: inline-block !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .visible-sm {
+    display: block !important;
+  }
+
+  table.visible-sm {
+    display: table !important;
+  }
+
+  tr.visible-sm {
+    display: table-row !important;
+  }
+
+  th.visible-sm,
+  td.visible-sm {
+    display: table-cell !important;
+  }
+}
+@media (min-width: 768px) and (max-width: 991px) {
+  .visible-sm-block {
+    display: block !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .visible-sm-inline {
+    display: inline !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .visible-sm-inline-block {
+    display: inline-block !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .visible-md {
+    display: block !important;
+  }
+
+  table.visible-md {
+    display: table !important;
+  }
+
+  tr.visible-md {
+    display: table-row !important;
+  }
+
+  th.visible-md,
+  td.visible-md {
+    display: table-cell !important;
+  }
+}
+@media (min-width: 992px) and (max-width: 1199px) {
+  .visible-md-block {
+    display: block !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .visible-md-inline {
+    display: inline !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .visible-md-inline-block {
+    display: inline-block !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .visible-lg {
+    display: block !important;
+  }
+
+  table.visible-lg {
+    display: table !important;
+  }
+
+  tr.visible-lg {
+    display: table-row !important;
+  }
+
+  th.visible-lg,
+  td.visible-lg {
+    display: table-cell !important;
+  }
+}
+@media (min-width: 1200px) {
+  .visible-lg-block {
+    display: block !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .visible-lg-inline {
+    display: inline !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .visible-lg-inline-block {
+    display: inline-block !important;
+  }
+}
+
+@media (max-width: 767px) {
+  .hidden-xs {
+    display: none !important;
+  }
+}
+@media (min-width: 768px) and (max-width: 991px) {
+  .hidden-sm {
+    display: none !important;
+  }
+}
+@media (min-width: 992px) and (max-width: 1199px) {
+  .hidden-md {
+    display: none !important;
+  }
+}
+@media (min-width: 1200px) {
+  .hidden-lg {
+    display: none !important;
+  }
+}
+.visible-print {
+  display: none !important;
+}
+
+@media print {
+  .visible-print {
+    display: block !important;
+  }
+
+  table.visible-print {
+    display: table !important;
+  }
+
+  tr.visible-print {
+    display: table-row !important;
+  }
+
+  th.visible-print,
+  td.visible-print {
+    display: table-cell !important;
+  }
+}
+.visible-print-block {
+  display: none !important;
+}
+@media print {
+  .visible-print-block {
+    display: block !important;
+  }
+}
+
+.visible-print-inline {
+  display: none !important;
+}
+@media print {
+  .visible-print-inline {
+    display: inline !important;
+  }
+}
+
+.visible-print-inline-block {
+  display: none !important;
+}
+@media print {
+  .visible-print-inline-block {
+    display: inline-block !important;
+  }
+}
+
+@media print {
+  .hidden-print {
+    display: none !important;
+  }
+}
+
diff --git a/website/themes/lektor-icon/assets/static/css/icomoon.css b/website/themes/lektor-icon/assets/static/css/icomoon.css
new file mode 100644
index 0000000..642ae95
--- /dev/null
+++ b/website/themes/lektor-icon/assets/static/css/icomoon.css
@@ -0,0 +1,2430 @@
+@font-face {
+  font-family: 'icomoon';
+  src: url("../fonts/icomoon/icomoon.woff?srf3rx") format("woff");
+  font-weight: normal;
+  font-style: normal;
+}
+
+[class^="icon-"], [class*=" icon-"] {
+	font-family: 'icomoon';
+	speak: none;
+	font-style: normal;
+	font-weight: normal;
+	font-variant: normal;
+	text-transform: none;
+	line-height: 1;
+}
+
+.icon-glass:before {
+	content: "\f000";
+}
+.icon-music:before {
+	content: "\f001";
+}
+.icon-search2:before {
+	content: "\f002";
+}
+.icon-envelope-o:before {
+	content: "\f003";
+}
+.icon-heart:before {
+	content: "\f004";
+}
+.icon-star:before {
+	content: "\f005";
+}
+.icon-star-o:before {
+	content: "\f006";
+}
+.icon-user:before {
+	content: "\f007";
+}
+.icon-film:before {
+	content: "\f008";
+}
+.icon-th-large:before {
+	content: "\f009";
+}
+.icon-th:before {
+	content: "\f00a";
+}
+.icon-th-list:before {
+	content: "\f00b";
+}
+.icon-check:before {
+	content: "\f00c";
+}
+.icon-close:before {
+	content: "\f00d";
+}
+.icon-remove:before {
+	content: "\f00d";
+}
+.icon-times:before {
+	content: "\f00d";
+}
+.icon-search-plus:before {
+	content: "\f00e";
+}
+.icon-search-minus:before {
+	content: "\f010";
+}
+.icon-power-off:before {
+	content: "\f011";
+}
+.icon-signal:before {
+	content: "\f012";
+}
+.icon-cog:before {
+	content: "\f013";
+}
+.icon-gear:before {
+	content: "\f013";
+}
+.icon-trash-o:before {
+	content: "\f014";
+}
+.icon-home:before {
+	content: "\f015";
+}
+.icon-file-o:before {
+	content: "\f016";
+}
+.icon-clock-o:before {
+	content: "\f017";
+}
+.icon-road:before {
+	content: "\f018";
+}
+.icon-download:before {
+	content: "\f019";
+}
+.icon-arrow-circle-o-down:before {
+	content: "\f01a";
+}
+.icon-arrow-circle-o-up:before {
+	content: "\f01b";
+}
+.icon-inbox:before {
+	content: "\f01c";
+}
+.icon-play-circle-o:before {
+	content: "\f01d";
+}
+.icon-repeat2:before {
+	content: "\f01e";
+}
+.icon-rotate-right:before {
+	content: "\f01e";
+}
+.icon-refresh:before {
+	content: "\f021";
+}
+.icon-list-alt:before {
+	content: "\f022";
+}
+.icon-lock:before {
+	content: "\f023";
+}
+.icon-flag:before {
+	content: "\f024";
+}
+.icon-headphones:before {
+	content: "\f025";
+}
+.icon-volume-off:before {
+	content: "\f026";
+}
+.icon-volume-down:before {
+	content: "\f027";
+}
+.icon-volume-up:before {
+	content: "\f028";
+}
+.icon-qrcode:before {
+	content: "\f029";
+}
+.icon-barcode:before {
+	content: "\f02a";
+}
+.icon-tag:before {
+	content: "\f02b";
+}
+.icon-tags:before {
+	content: "\f02c";
+}
+.icon-book:before {
+	content: "\f02d";
+}
+.icon-bookmark:before {
+	content: "\f02e";
+}
+.icon-print:before {
+	content: "\f02f";
+}
+.icon-camera:before {
+	content: "\f030";
+}
+.icon-font:before {
+	content: "\f031";
+}
+.icon-bold:before {
+	content: "\f032";
+}
+.icon-italic:before {
+	content: "\f033";
+}
+.icon-text-height:before {
+	content: "\f034";
+}
+.icon-text-width:before {
+	content: "\f035";
+}
+.icon-align-left:before {
+	content: "\f036";
+}
+.icon-align-center2:before {
+	content: "\f037";
+}
+.icon-align-right:before {
+	content: "\f038";
+}
+.icon-align-justify2:before {
+	content: "\f039";
+}
+.icon-list:before {
+	content: "\f03a";
+}
+.icon-dedent:before {
+	content: "\f03b";
+}
+.icon-outdent:before {
+	content: "\f03b";
+}
+.icon-indent:before {
+	content: "\f03c";
+}
+.icon-video-camera:before {
+	content: "\f03d";
+}
+.icon-image:before {
+	content: "\f03e";
+}
+.icon-photo:before {
+	content: "\f03e";
+}
+.icon-picture-o:before {
+	content: "\f03e";
+}
+.icon-pencil:before {
+	content: "\f040";
+}
+.icon-map-marker:before {
+	content: "\f041";
+}
+.icon-adjust:before {
+	content: "\f042";
+}
+.icon-tint:before {
+	content: "\f043";
+}
+.icon-edit:before {
+	content: "\f044";
+}
+.icon-pencil-square-o:before {
+	content: "\f044";
+}
+.icon-share-square-o:before {
+	content: "\f045";
+}
+.icon-check-square-o:before {
+	content: "\f046";
+}
+.icon-arrows:before {
+	content: "\f047";
+}
+.icon-step-backward:before {
+	content: "\f048";
+}
+.icon-fast-backward:before {
+	content: "\f049";
+}
+.icon-backward:before {
+	content: "\f04a";
+}
+.icon-play2:before {
+	content: "\f04b";
+}
+.icon-pause2:before {
+	content: "\f04c";
+}
+.icon-stop2:before {
+	content: "\f04d";
+}
+.icon-forward:before {
+	content: "\f04e";
+}
+.icon-fast-forward2:before {
+	content: "\f050";
+}
+.icon-step-forward:before {
+	content: "\f051";
+}
+.icon-eject:before {
+	content: "\f052";
+}
+.icon-chevron-left:before {
+	content: "\f053";
+}
+.icon-chevron-right:before {
+	content: "\f054";
+}
+.icon-plus-circle:before {
+	content: "\f055";
+}
+.icon-minus-circle:before {
+	content: "\f056";
+}
+.icon-times-circle:before {
+	content: "\f057";
+}
+.icon-check-circle:before {
+	content: "\f058";
+}
+.icon-question-circle:before {
+	content: "\f059";
+}
+.icon-info-circle:before {
+	content: "\f05a";
+}
+.icon-crosshairs:before {
+	content: "\f05b";
+}
+.icon-times-circle-o:before {
+	content: "\f05c";
+}
+.icon-check-circle-o:before {
+	content: "\f05d";
+}
+.icon-ban2:before {
+	content: "\f05e";
+}
+.icon-arrow-left:before {
+	content: "\f060";
+}
+.icon-arrow-right:before {
+	content: "\f061";
+}
+.icon-arrow-up:before {
+	content: "\f062";
+}
+.icon-arrow-down:before {
+	content: "\f063";
+}
+.icon-mail-forward:before {
+	content: "\f064";
+}
+.icon-share:before {
+	content: "\f064";
+}
+.icon-expand2:before {
+	content: "\f065";
+}
+.icon-compress:before {
+	content: "\f066";
+}
+.icon-plus:before {
+	content: "\f067";
+}
+.icon-minus:before {
+	content: "\f068";
+}
+.icon-asterisk:before {
+	content: "\f069";
+}
+.icon-exclamation-circle:before {
+	content: "\f06a";
+}
+.icon-gift:before {
+	content: "\f06b";
+}
+.icon-leaf:before {
+	content: "\f06c";
+}
+.icon-fire:before {
+	content: "\f06d";
+}
+.icon-eye:before {
+	content: "\f06e";
+}
+.icon-eye-slash:before {
+	content: "\f070";
+}
+.icon-exclamation-triangle:before {
+	content: "\f071";
+}
+.icon-warning:before {
+	content: "\f071";
+}
+.icon-plane:before {
+	content: "\f072";
+}
+.icon-calendar:before {
+	content: "\f073";
+}
+.icon-random:before {
+	content: "\f074";
+}
+.icon-comment:before {
+	content: "\f075";
+}
+.icon-magnet:before {
+	content: "\f076";
+}
+.icon-chevron-up:before {
+	content: "\f077";
+}
+.icon-chevron-down:before {
+	content: "\f078";
+}
+.icon-retweet:before {
+	content: "\f079";
+}
+.icon-shopping-cart:before {
+	content: "\f07a";
+}
+.icon-folder:before {
+	content: "\f07b";
+}
+.icon-folder-open:before {
+	content: "\f07c";
+}
+.icon-arrows-v:before {
+	content: "\f07d";
+}
+.icon-arrows-h:before {
+	content: "\f07e";
+}
+.icon-bar-chart:before {
+	content: "\f080";
+}
+.icon-bar-chart-o:before {
+	content: "\f080";
+}
+.icon-twitter-square:before {
+	content: "\f081";
+}
+.icon-facebook-square:before {
+	content: "\f082";
+}
+.icon-camera-retro:before {
+	content: "\f083";
+}
+.icon-key:before {
+	content: "\f084";
+}
+.icon-cogs:before {
+	content: "\f085";
+}
+.icon-gears:before {
+	content: "\f085";
+}
+.icon-comments:before {
+	content: "\f086";
+}
+.icon-thumbs-o-up:before {
+	content: "\f087";
+}
+.icon-thumbs-o-down:before {
+	content: "\f088";
+}
+.icon-star-half:before {
+	content: "\f089";
+}
+.icon-heart-o:before {
+	content: "\f08a";
+}
+.icon-sign-out:before {
+	content: "\f08b";
+}
+.icon-linkedin-square:before {
+	content: "\f08c";
+}
+.icon-thumb-tack:before {
+	content: "\f08d";
+}
+.icon-external-link:before {
+	content: "\f08e";
+}
+.icon-sign-in:before {
+	content: "\f090";
+}
+.icon-trophy:before {
+	content: "\f091";
+}
+.icon-github-square:before {
+	content: "\f092";
+}
+.icon-upload:before {
+	content: "\f093";
+}
+.icon-lemon-o:before {
+	content: "\f094";
+}
+.icon-phone:before {
+	content: "\f095";
+}
+.icon-square-o:before {
+	content: "\f096";
+}
+.icon-bookmark-o:before {
+	content: "\f097";
+}
+.icon-phone-square:before {
+	content: "\f098";
+}
+.icon-twitter:before {
+	content: "\f099";
+}
+.icon-facebook:before {
+	content: "\f09a";
+}
+.icon-facebook-f:before {
+	content: "\f09a";
+}
+.icon-github:before {
+	content: "\f09b";
+}
+.icon-unlock2:before {
+	content: "\f09c";
+}
+.icon-credit-card:before {
+	content: "\f09d";
+}
+.icon-feed:before {
+	content: "\f09e";
+}
+.icon-rss:before {
+	content: "\f09e";
+}
+.icon-hdd-o:before {
+	content: "\f0a0";
+}
+.icon-bullhorn:before {
+	content: "\f0a1";
+}
+.icon-bell-o:before {
+	content: "\f0a2";
+}
+.icon-certificate:before {
+	content: "\f0a3";
+}
+.icon-hand-o-right:before {
+	content: "\f0a4";
+}
+.icon-hand-o-left:before {
+	content: "\f0a5";
+}
+.icon-hand-o-up:before {
+	content: "\f0a6";
+}
+.icon-hand-o-down:before {
+	content: "\f0a7";
+}
+.icon-arrow-circle-left:before {
+	content: "\f0a8";
+}
+.icon-arrow-circle-right:before {
+	content: "\f0a9";
+}
+.icon-arrow-circle-up:before {
+	content: "\f0aa";
+}
+.icon-arrow-circle-down:before {
+	content: "\f0ab";
+}
+.icon-globe:before {
+	content: "\f0ac";
+}
+.icon-wrench:before {
+	content: "\f0ad";
+}
+.icon-tasks:before {
+	content: "\f0ae";
+}
+.icon-filter:before {
+	content: "\f0b0";
+}
+.icon-briefcase:before {
+	content: "\f0b1";
+}
+.icon-arrows-alt:before {
+	content: "\f0b2";
+}
+.icon-group:before {
+	content: "\f0c0";
+}
+.icon-users:before {
+	content: "\f0c0";
+}
+.icon-chain:before {
+	content: "\f0c1";
+}
+.icon-link:before {
+	content: "\f0c1";
+}
+.icon-cloud:before {
+	content: "\f0c2";
+}
+.icon-flask:before {
+	content: "\f0c3";
+}
+.icon-cut:before {
+	content: "\f0c4";
+}
+.icon-scissors:before {
+	content: "\f0c4";
+}
+.icon-copy:before {
+	content: "\f0c5";
+}
+.icon-files-o:before {
+	content: "\f0c5";
+}
+.icon-paperclip:before {
+	content: "\f0c6";
+}
+.icon-floppy-o:before {
+	content: "\f0c7";
+}
+.icon-save:before {
+	content: "\f0c7";
+}
+.icon-square:before {
+	content: "\f0c8";
+}
+.icon-bars:before {
+	content: "\f0c9";
+}
+.icon-navicon:before {
+	content: "\f0c9";
+}
+.icon-reorder:before {
+	content: "\f0c9";
+}
+.icon-list-ul:before {
+	content: "\f0ca";
+}
+.icon-list-ol:before {
+	content: "\f0cb";
+}
+.icon-strikethrough:before {
+	content: "\f0cc";
+}
+.icon-underline:before {
+	content: "\f0cd";
+}
+.icon-table:before {
+	content: "\f0ce";
+}
+.icon-magic:before {
+	content: "\f0d0";
+}
+.icon-truck:before {
+	content: "\f0d1";
+}
+.icon-pinterest:before {
+	content: "\f0d2";
+}
+.icon-pinterest-square:before {
+	content: "\f0d3";
+}
+.icon-google-plus-square:before {
+	content: "\f0d4";
+}
+.icon-google-plus:before {
+	content: "\f0d5";
+}
+.icon-money:before {
+	content: "\f0d6";
+}
+.icon-caret-down:before {
+	content: "\f0d7";
+}
+.icon-caret-up:before {
+	content: "\f0d8";
+}
+.icon-caret-left:before {
+	content: "\f0d9";
+}
+.icon-caret-right:before {
+	content: "\f0da";
+}
+.icon-columns2:before {
+	content: "\f0db";
+}
+.icon-sort:before {
+	content: "\f0dc";
+}
+.icon-unsorted:before {
+	content: "\f0dc";
+}
+.icon-sort-desc:before {
+	content: "\f0dd";
+}
+.icon-sort-down:before {
+	content: "\f0dd";
+}
+.icon-sort-asc:before {
+	content: "\f0de";
+}
+.icon-sort-up:before {
+	content: "\f0de";
+}
+.icon-envelope:before {
+	content: "\f0e0";
+}
+.icon-linkedin:before {
+	content: "\f0e1";
+}
+.icon-rotate-left:before {
+	content: "\f0e2";
+}
+.icon-undo:before {
+	content: "\f0e2";
+}
+.icon-gavel:before {
+	content: "\f0e3";
+}
+.icon-legal:before {
+	content: "\f0e3";
+}
+.icon-dashboard:before {
+	content: "\f0e4";
+}
+.icon-tachometer:before {
+	content: "\f0e4";
+}
+.icon-comment-o:before {
+	content: "\f0e5";
+}
+.icon-comments-o:before {
+	content: "\f0e6";
+}
+.icon-bolt:before {
+	content: "\f0e7";
+}
+.icon-flash:before {
+	content: "\f0e7";
+}
+.icon-sitemap:before {
+	content: "\f0e8";
+}
+.icon-umbrella2:before {
+	content: "\f0e9";
+}
+.icon-clipboard:before {
+	content: "\f0ea";
+}
+.icon-paste:before {
+	content: "\f0ea";
+}
+.icon-lightbulb-o:before {
+	content: "\f0eb";
+}
+.icon-exchange:before {
+	content: "\f0ec";
+}
+.icon-cloud-download2:before {
+	content: "\f0ed";
+}
+.icon-cloud-upload2:before {
+	content: "\f0ee";
+}
+.icon-user-md:before {
+	content: "\f0f0";
+}
+.icon-stethoscope:before {
+	content: "\f0f1";
+}
+.icon-suitcase:before {
+	content: "\f0f2";
+}
+.icon-bell:before {
+	content: "\f0f3";
+}
+.icon-coffee:before {
+	content: "\f0f4";
+}
+.icon-cutlery:before {
+	content: "\f0f5";
+}
+.icon-file-text-o:before {
+	content: "\f0f6";
+}
+.icon-building-o:before {
+	content: "\f0f7";
+}
+.icon-hospital-o:before {
+	content: "\f0f8";
+}
+.icon-ambulance:before {
+	content: "\f0f9";
+}
+.icon-medkit:before {
+	content: "\f0fa";
+}
+.icon-fighter-jet:before {
+	content: "\f0fb";
+}
+.icon-beer:before {
+	content: "\f0fc";
+}
+.icon-h-square:before {
+	content: "\f0fd";
+}
+.icon-plus-square:before {
+	content: "\f0fe";
+}
+.icon-angle-double-left:before {
+	content: "\f100";
+}
+.icon-angle-double-right:before {
+	content: "\f101";
+}
+.icon-angle-double-up:before {
+	content: "\f102";
+}
+.icon-angle-double-down:before {
+	content: "\f103";
+}
+.icon-angle-left:before {
+	content: "\f104";
+}
+.icon-angle-right:before {
+	content: "\f105";
+}
+.icon-angle-up:before {
+	content: "\f106";
+}
+.icon-angle-down:before {
+	content: "\f107";
+}
+.icon-desktop:before {
+	content: "\f108";
+}
+.icon-laptop:before {
+	content: "\f109";
+}
+.icon-tablet:before {
+	content: "\f10a";
+}
+.icon-mobile:before {
+	content: "\f10b";
+}
+.icon-mobile-phone:before {
+	content: "\f10b";
+}
+.icon-circle-o:before {
+	content: "\f10c";
+}
+.icon-quote-left:before {
+	content: "\f10d";
+}
+.icon-quote-right:before {
+	content: "\f10e";
+}
+.icon-spinner:before {
+	content: "\f110";
+}
+.icon-circle:before {
+	content: "\f111";
+}
+.icon-mail-reply:before {
+	content: "\f112";
+}
+.icon-reply:before {
+	content: "\f112";
+}
+.icon-github-alt:before {
+	content: "\f113";
+}
+.icon-folder-o:before {
+	content: "\f114";
+}
+.icon-folder-open-o:before {
+	content: "\f115";
+}
+.icon-smile-o:before {
+	content: "\f118";
+}
+.icon-frown-o:before {
+	content: "\f119";
+}
+.icon-meh-o:before {
+	content: "\f11a";
+}
+.icon-gamepad:before {
+	content: "\f11b";
+}
+.icon-keyboard-o:before {
+	content: "\f11c";
+}
+.icon-flag-o:before {
+	content: "\f11d";
+}
+.icon-flag-checkered:before {
+	content: "\f11e";
+}
+.icon-terminal:before {
+	content: "\f120";
+}
+.icon-code:before {
+	content: "\f121";
+}
+.icon-mail-reply-all:before {
+	content: "\f122";
+}
+.icon-reply-all:before {
+	content: "\f122";
+}
+.icon-star-half-empty:before {
+	content: "\f123";
+}
+.icon-star-half-full:before {
+	content: "\f123";
+}
+.icon-star-half-o:before {
+	content: "\f123";
+}
+.icon-location-arrow:before {
+	content: "\f124";
+}
+.icon-crop:before {
+	content: "\f125";
+}
+.icon-code-fork:before {
+	content: "\f126";
+}
+.icon-chain-broken:before {
+	content: "\f127";
+}
+.icon-unlink:before {
+	content: "\f127";
+}
+.icon-question:before {
+	content: "\f128";
+}
+.icon-info:before {
+	content: "\f129";
+}
+.icon-exclamation:before {
+	content: "\f12a";
+}
+.icon-superscript:before {
+	content: "\f12b";
+}
+.icon-subscript:before {
+	content: "\f12c";
+}
+.icon-eraser:before {
+	content: "\f12d";
+}
+.icon-puzzle-piece:before {
+	content: "\f12e";
+}
+.icon-microphone2:before {
+	content: "\f130";
+}
+.icon-microphone-slash:before {
+	content: "\f131";
+}
+.icon-shield:before {
+	content: "\f132";
+}
+.icon-calendar-o:before {
+	content: "\f133";
+}
+.icon-fire-extinguisher:before {
+	content: "\f134";
+}
+.icon-rocket:before {
+	content: "\f135";
+}
+.icon-maxcdn:before {
+	content: "\f136";
+}
+.icon-chevron-circle-left:before {
+	content: "\f137";
+}
+.icon-chevron-circle-right:before {
+	content: "\f138";
+}
+.icon-chevron-circle-up:before {
+	content: "\f139";
+}
+.icon-chevron-circle-down:before {
+	content: "\f13a";
+}
+.icon-html5:before {
+	content: "\f13b";
+}
+.icon-css3:before {
+	content: "\f13c";
+}
+.icon-anchor2:before {
+	content: "\f13d";
+}
+.icon-unlock-alt:before {
+	content: "\f13e";
+}
+.icon-bullseye:before {
+	content: "\f140";
+}
+.icon-ellipsis-h:before {
+	content: "\f141";
+}
+.icon-ellipsis-v:before {
+	content: "\f142";
+}
+.icon-rss-square:before {
+	content: "\f143";
+}
+.icon-play-circle:before {
+	content: "\f144";
+}
+.icon-ticket:before {
+	content: "\f145";
+}
+.icon-minus-square:before {
+	content: "\f146";
+}
+.icon-minus-square-o:before {
+	content: "\f147";
+}
+.icon-level-up:before {
+	content: "\f148";
+}
+.icon-level-down:before {
+	content: "\f149";
+}
+.icon-check-square:before {
+	content: "\f14a";
+}
+.icon-pencil-square:before {
+	content: "\f14b";
+}
+.icon-external-link-square:before {
+	content: "\f14c";
+}
+.icon-share-square:before {
+	content: "\f14d";
+}
+.icon-compass:before {
+	content: "\f14e";
+}
+.icon-caret-square-o-down:before {
+	content: "\f150";
+}
+.icon-toggle-down:before {
+	content: "\f150";
+}
+.icon-caret-square-o-up:before {
+	content: "\f151";
+}
+.icon-toggle-up:before {
+	content: "\f151";
+}
+.icon-caret-square-o-right:before {
+	content: "\f152";
+}
+.icon-toggle-right:before {
+	content: "\f152";
+}
+.icon-eur:before {
+	content: "\f153";
+}
+.icon-euro:before {
+	content: "\f153";
+}
+.icon-gbp:before {
+	content: "\f154";
+}
+.icon-dollar:before {
+	content: "\f155";
+}
+.icon-usd:before {
+	content: "\f155";
+}
+.icon-inr:before {
+	content: "\f156";
+}
+.icon-rupee:before {
+	content: "\f156";
+}
+.icon-cny:before {
+	content: "\f157";
+}
+.icon-jpy:before {
+	content: "\f157";
+}
+.icon-rmb:before {
+	content: "\f157";
+}
+.icon-yen:before {
+	content: "\f157";
+}
+.icon-rouble:before {
+	content: "\f158";
+}
+.icon-rub:before {
+	content: "\f158";
+}
+.icon-ruble:before {
+	content: "\f158";
+}
+.icon-krw:before {
+	content: "\f159";
+}
+.icon-won:before {
+	content: "\f159";
+}
+.icon-bitcoin:before {
+	content: "\f15a";
+}
+.icon-btc:before {
+	content: "\f15a";
+}
+.icon-file2:before {
+	content: "\f15b";
+}
+.icon-file-text:before {
+	content: "\f15c";
+}
+.icon-sort-alpha-asc:before {
+	content: "\f15d";
+}
+.icon-sort-alpha-desc:before {
+	content: "\f15e";
+}
+.icon-sort-amount-asc:before {
+	content: "\f160";
+}
+.icon-sort-amount-desc:before {
+	content: "\f161";
+}
+.icon-sort-numeric-asc:before {
+	content: "\f162";
+}
+.icon-sort-numeric-desc:before {
+	content: "\f163";
+}
+.icon-thumbs-up:before {
+	content: "\f164";
+}
+.icon-thumbs-down:before {
+	content: "\f165";
+}
+.icon-youtube-square:before {
+	content: "\f166";
+}
+.icon-youtube:before {
+	content: "\f167";
+}
+.icon-xing:before {
+	content: "\f168";
+}
+.icon-xing-square:before {
+	content: "\f169";
+}
+.icon-youtube-play:before {
+	content: "\f16a";
+}
+.icon-dropbox:before {
+	content: "\f16b";
+}
+.icon-stack-overflow:before {
+	content: "\f16c";
+}
+.icon-instagram:before {
+	content: "\f16d";
+}
+.icon-flickr:before {
+	content: "\f16e";
+}
+.icon-adn:before {
+	content: "\f170";
+}
+.icon-bitbucket:before {
+	content: "\f171";
+}
+.icon-bitbucket-square:before {
+	content: "\f172";
+}
+.icon-tumblr:before {
+	content: "\f173";
+}
+.icon-tumblr-square:before {
+	content: "\f174";
+}
+.icon-long-arrow-down:before {
+	content: "\f175";
+}
+.icon-long-arrow-up:before {
+	content: "\f176";
+}
+.icon-long-arrow-left:before {
+	content: "\f177";
+}
+.icon-long-arrow-right:before {
+	content: "\f178";
+}
+.icon-apple:before {
+	content: "\f179";
+}
+.icon-windows:before {
+	content: "\f17a";
+}
+.icon-android:before {
+	content: "\f17b";
+}
+.icon-linux:before {
+	content: "\f17c";
+}
+.icon-dribbble:before {
+	content: "\f17d";
+}
+.icon-skype:before {
+	content: "\f17e";
+}
+.icon-foursquare:before {
+	content: "\f180";
+}
+.icon-trello:before {
+	content: "\f181";
+}
+.icon-female:before {
+	content: "\f182";
+}
+.icon-male:before {
+	content: "\f183";
+}
+.icon-gittip:before {
+	content: "\f184";
+}
+.icon-gratipay:before {
+	content: "\f184";
+}
+.icon-sun-o:before {
+	content: "\f185";
+}
+.icon-moon-o:before {
+	content: "\f186";
+}
+.icon-archive:before {
+	content: "\f187";
+}
+.icon-bug:before {
+	content: "\f188";
+}
+.icon-vk:before {
+	content: "\f189";
+}
+.icon-weibo:before {
+	content: "\f18a";
+}
+.icon-renren:before {
+	content: "\f18b";
+}
+.icon-pagelines:before {
+	content: "\f18c";
+}
+.icon-stack-exchange:before {
+	content: "\f18d";
+}
+.icon-arrow-circle-o-right:before {
+	content: "\f18e";
+}
+.icon-arrow-circle-o-left:before {
+	content: "\f190";
+}
+.icon-caret-square-o-left:before {
+	content: "\f191";
+}
+.icon-toggle-left:before {
+	content: "\f191";
+}
+.icon-dot-circle-o:before {
+	content: "\f192";
+}
+.icon-wheelchair:before {
+	content: "\f193";
+}
+.icon-vimeo-square:before {
+	content: "\f194";
+}
+.icon-try:before {
+	content: "\f195";
+}
+.icon-turkish-lira:before {
+	content: "\f195";
+}
+.icon-plus-square-o:before {
+	content: "\f196";
+}
+.icon-space-shuttle:before {
+	content: "\f197";
+}
+.icon-slack:before {
+	content: "\f198";
+}
+.icon-envelope-square:before {
+	content: "\f199";
+}
+.icon-wordpress:before {
+	content: "\f19a";
+}
+.icon-openid:before {
+	content: "\f19b";
+}
+.icon-bank:before {
+	content: "\f19c";
+}
+.icon-institution:before {
+	content: "\f19c";
+}
+.icon-university:before {
+	content: "\f19c";
+}
+.icon-graduation-cap:before {
+	content: "\f19d";
+}
+.icon-mortar-board:before {
+	content: "\f19d";
+}
+.icon-yahoo:before {
+	content: "\f19e";
+}
+.icon-google:before {
+	content: "\f1a0";
+}
+.icon-reddit:before {
+	content: "\f1a1";
+}
+.icon-reddit-square:before {
+	content: "\f1a2";
+}
+.icon-stumbleupon-circle:before {
+	content: "\f1a3";
+}
+.icon-stumbleupon:before {
+	content: "\f1a4";
+}
+.icon-delicious:before {
+	content: "\f1a5";
+}
+.icon-digg:before {
+	content: "\f1a6";
+}
+.icon-pied-piper:before {
+	content: "\f1a7";
+}
+.icon-pied-piper-alt:before {
+	content: "\f1a8";
+}
+.icon-drupal:before {
+	content: "\f1a9";
+}
+.icon-joomla:before {
+	content: "\f1aa";
+}
+.icon-language:before {
+	content: "\f1ab";
+}
+.icon-fax:before {
+	content: "\f1ac";
+}
+.icon-building:before {
+	content: "\f1ad";
+}
+.icon-child:before {
+	content: "\f1ae";
+}
+.icon-paw:before {
+	content: "\f1b0";
+}
+.icon-spoon:before {
+	content: "\f1b1";
+}
+.icon-cube:before {
+	content: "\f1b2";
+}
+.icon-cubes:before {
+	content: "\f1b3";
+}
+.icon-behance:before {
+	content: "\f1b4";
+}
+.icon-behance-square:before {
+	content: "\f1b5";
+}
+.icon-steam:before {
+	content: "\f1b6";
+}
+.icon-steam-square:before {
+	content: "\f1b7";
+}
+.icon-recycle:before {
+	content: "\f1b8";
+}
+.icon-automobile:before {
+	content: "\f1b9";
+}
+.icon-car:before {
+	content: "\f1b9";
+}
+.icon-cab:before {
+	content: "\f1ba";
+}
+.icon-taxi:before {
+	content: "\f1ba";
+}
+.icon-tree:before {
+	content: "\f1bb";
+}
+.icon-spotify:before {
+	content: "\f1bc";
+}
+.icon-deviantart:before {
+	content: "\f1bd";
+}
+.icon-soundcloud:before {
+	content: "\f1be";
+}
+.icon-database:before {
+	content: "\f1c0";
+}
+.icon-file-pdf-o:before {
+	content: "\f1c1";
+}
+.icon-file-word-o:before {
+	content: "\f1c2";
+}
+.icon-file-excel-o:before {
+	content: "\f1c3";
+}
+.icon-file-powerpoint-o:before {
+	content: "\f1c4";
+}
+.icon-file-image-o:before {
+	content: "\f1c5";
+}
+.icon-file-photo-o:before {
+	content: "\f1c5";
+}
+.icon-file-picture-o:before {
+	content: "\f1c5";
+}
+.icon-file-archive-o:before {
+	content: "\f1c6";
+}
+.icon-file-zip-o:before {
+	content: "\f1c6";
+}
+.icon-file-audio-o:before {
+	content: "\f1c7";
+}
+.icon-file-sound-o:before {
+	content: "\f1c7";
+}
+.icon-file-movie-o:before {
+	content: "\f1c8";
+}
+.icon-file-video-o:before {
+	content: "\f1c8";
+}
+.icon-file-code-o:before {
+	content: "\f1c9";
+}
+.icon-vine:before {
+	content: "\f1ca";
+}
+.icon-codepen:before {
+	content: "\f1cb";
+}
+.icon-jsfiddle:before {
+	content: "\f1cc";
+}
+.icon-life-bouy:before {
+	content: "\f1cd";
+}
+.icon-life-buoy:before {
+	content: "\f1cd";
+}
+.icon-life-ring:before {
+	content: "\f1cd";
+}
+.icon-life-saver:before {
+	content: "\f1cd";
+}
+.icon-support:before {
+	content: "\f1cd";
+}
+.icon-circle-o-notch:before {
+	content: "\f1ce";
+}
+.icon-ra:before {
+	content: "\f1d0";
+}
+.icon-rebel:before {
+	content: "\f1d0";
+}
+.icon-empire:before {
+	content: "\f1d1";
+}
+.icon-ge:before {
+	content: "\f1d1";
+}
+.icon-git-square:before {
+	content: "\f1d2";
+}
+.icon-git:before {
+	content: "\f1d3";
+}
+.icon-hacker-news:before {
+	content: "\f1d4";
+}
+.icon-y-combinator-square:before {
+	content: "\f1d4";
+}
+.icon-yc-square:before {
+	content: "\f1d4";
+}
+.icon-tencent-weibo:before {
+	content: "\f1d5";
+}
+.icon-qq:before {
+	content: "\f1d6";
+}
+.icon-wechat:before {
+	content: "\f1d7";
+}
+.icon-weixin:before {
+	content: "\f1d7";
+}
+.icon-paper-plane:before {
+	content: "\f1d8";
+}
+.icon-send:before {
+	content: "\f1d8";
+}
+.icon-paper-plane-o:before {
+	content: "\f1d9";
+}
+.icon-send-o:before {
+	content: "\f1d9";
+}
+.icon-history:before {
+	content: "\f1da";
+}
+.icon-circle-thin:before {
+	content: "\f1db";
+}
+.icon-header:before {
+	content: "\f1dc";
+}
+.icon-paragraph2:before {
+	content: "\f1dd";
+}
+.icon-sliders:before {
+	content: "\f1de";
+}
+.icon-share-alt:before {
+	content: "\f1e0";
+}
+.icon-share-alt-square:before {
+	content: "\f1e1";
+}
+.icon-bomb:before {
+	content: "\f1e2";
+}
+.icon-futbol-o:before {
+	content: "\f1e3";
+}
+.icon-soccer-ball-o:before {
+	content: "\f1e3";
+}
+.icon-tty:before {
+	content: "\f1e4";
+}
+.icon-binoculars:before {
+	content: "\f1e5";
+}
+.icon-plug:before {
+	content: "\f1e6";
+}
+.icon-slideshare:before {
+	content: "\f1e7";
+}
+.icon-twitch:before {
+	content: "\f1e8";
+}
+.icon-yelp:before {
+	content: "\f1e9";
+}
+.icon-newspaper-o:before {
+	content: "\f1ea";
+}
+.icon-wifi:before {
+	content: "\f1eb";
+}
+.icon-calculator:before {
+	content: "\f1ec";
+}
+.icon-paypal:before {
+	content: "\f1ed";
+}
+.icon-google-wallet:before {
+	content: "\f1ee";
+}
+.icon-cc-visa:before {
+	content: "\f1f0";
+}
+.icon-cc-mastercard:before {
+	content: "\f1f1";
+}
+.icon-cc-discover:before {
+	content: "\f1f2";
+}
+.icon-cc-amex:before {
+	content: "\f1f3";
+}
+.icon-cc-paypal:before {
+	content: "\f1f4";
+}
+.icon-cc-stripe:before {
+	content: "\f1f5";
+}
+.icon-bell-slash:before {
+	content: "\f1f6";
+}
+.icon-bell-slash-o:before {
+	content: "\f1f7";
+}
+.icon-trash:before {
+	content: "\f1f8";
+}
+.icon-copyright:before {
+	content: "\f1f9";
+}
+.icon-at:before {
+	content: "\f1fa";
+}
+.icon-eyedropper:before {
+	content: "\f1fb";
+}
+.icon-paint-brush:before {
+	content: "\f1fc";
+}
+.icon-birthday-cake:before {
+	content: "\f1fd";
+}
+.icon-area-chart:before {
+	content: "\f1fe";
+}
+.icon-pie-chart:before {
+	content: "\f200";
+}
+.icon-line-chart:before {
+	content: "\f201";
+}
+.icon-lastfm:before {
+	content: "\f202";
+}
+.icon-lastfm-square:before {
+	content: "\f203";
+}
+.icon-toggle-off:before {
+	content: "\f204";
+}
+.icon-toggle-on:before {
+	content: "\f205";
+}
+.icon-bicycle:before {
+	content: "\f206";
+}
+.icon-bus:before {
+	content: "\f207";
+}
+.icon-ioxhost:before {
+	content: "\f208";
+}
+.icon-angellist:before {
+	content: "\f209";
+}
+.icon-cc:before {
+	content: "\f20a";
+}
+.icon-ils:before {
+	content: "\f20b";
+}
+.icon-shekel:before {
+	content: "\f20b";
+}
+.icon-sheqel:before {
+	content: "\f20b";
+}
+.icon-meanpath:before {
+	content: "\f20c";
+}
+.icon-buysellads:before {
+	content: "\f20d";
+}
+.icon-connectdevelop:before {
+	content: "\f20e";
+}
+.icon-dashcube:before {
+	content: "\f210";
+}
+.icon-forumbee:before {
+	content: "\f211";
+}
+.icon-leanpub:before {
+	content: "\f212";
+}
+.icon-sellsy:before {
+	content: "\f213";
+}
+.icon-shirtsinbulk:before {
+	content: "\f214";
+}
+.icon-simplybuilt:before {
+	content: "\f215";
+}
+.icon-skyatlas:before {
+	content: "\f216";
+}
+.icon-cart-plus:before {
+	content: "\f217";
+}
+.icon-cart-arrow-down:before {
+	content: "\f218";
+}
+.icon-diamond:before {
+	content: "\f219";
+}
+.icon-ship:before {
+	content: "\f21a";
+}
+.icon-user-secret:before {
+	content: "\f21b";
+}
+.icon-motorcycle:before {
+	content: "\f21c";
+}
+.icon-street-view:before {
+	content: "\f21d";
+}
+.icon-heartbeat:before {
+	content: "\f21e";
+}
+.icon-venus:before {
+	content: "\f221";
+}
+.icon-mars:before {
+	content: "\f222";
+}
+.icon-mercury:before {
+	content: "\f223";
+}
+.icon-intersex:before {
+	content: "\f224";
+}
+.icon-transgender:before {
+	content: "\f224";
+}
+.icon-transgender-alt:before {
+	content: "\f225";
+}
+.icon-venus-double:before {
+	content: "\f226";
+}
+.icon-mars-double:before {
+	content: "\f227";
+}
+.icon-venus-mars:before {
+	content: "\f228";
+}
+.icon-mars-stroke:before {
+	content: "\f229";
+}
+.icon-mars-stroke-v:before {
+	content: "\f22a";
+}
+.icon-mars-stroke-h:before {
+	content: "\f22b";
+}
+.icon-neuter:before {
+	content: "\f22c";
+}
+.icon-genderless:before {
+	content: "\f22d";
+}
+.icon-facebook-official:before {
+	content: "\f230";
+}
+.icon-pinterest-p:before {
+	content: "\f231";
+}
+.icon-whatsapp:before {
+	content: "\f232";
+}
+.icon-server2:before {
+	content: "\f233";
+}
+.icon-user-plus:before {
+	content: "\f234";
+}
+.icon-user-times:before {
+	content: "\f235";
+}
+.icon-bed:before {
+	content: "\f236";
+}
+.icon-hotel:before {
+	content: "\f236";
+}
+.icon-viacoin:before {
+	content: "\f237";
+}
+.icon-train:before {
+	content: "\f238";
+}
+.icon-subway:before {
+	content: "\f239";
+}
+.icon-medium:before {
+	content: "\f23a";
+}
+.icon-y-combinator:before {
+	content: "\f23b";
+}
+.icon-yc:before {
+	content: "\f23b";
+}
+.icon-optin-monster:before {
+	content: "\f23c";
+}
+.icon-opencart:before {
+	content: "\f23d";
+}
+.icon-expeditedssl:before {
+	content: "\f23e";
+}
+.icon-battery-4:before {
+	content: "\f240";
+}
+.icon-battery-full:before {
+	content: "\f240";
+}
+.icon-battery-3:before {
+	content: "\f241";
+}
+.icon-battery-three-quarters:before {
+	content: "\f241";
+}
+.icon-battery-2:before {
+	content: "\f242";
+}
+.icon-battery-half:before {
+	content: "\f242";
+}
+.icon-battery-1:before {
+	content: "\f243";
+}
+.icon-battery-quarter:before {
+	content: "\f243";
+}
+.icon-battery-0:before {
+	content: "\f244";
+}
+.icon-battery-empty:before {
+	content: "\f244";
+}
+.icon-mouse-pointer:before {
+	content: "\f245";
+}
+.icon-i-cursor:before {
+	content: "\f246";
+}
+.icon-object-group:before {
+	content: "\f247";
+}
+.icon-object-ungroup:before {
+	content: "\f248";
+}
+.icon-sticky-note:before {
+	content: "\f249";
+}
+.icon-sticky-note-o:before {
+	content: "\f24a";
+}
+.icon-cc-jcb:before {
+	content: "\f24b";
+}
+.icon-cc-diners-club:before {
+	content: "\f24c";
+}
+.icon-clone:before {
+	content: "\f24d";
+}
+.icon-balance-scale:before {
+	content: "\f24e";
+}
+.icon-hourglass-o:before {
+	content: "\f250";
+}
+.icon-hourglass-1:before {
+	content: "\f251";
+}
+.icon-hourglass-start:before {
+	content: "\f251";
+}
+.icon-hourglass-2:before {
+	content: "\f252";
+}
+.icon-hourglass-half:before {
+	content: "\f252";
+}
+.icon-hourglass-3:before {
+	content: "\f253";
+}
+.icon-hourglass-end:before {
+	content: "\f253";
+}
+.icon-hourglass:before {
+	content: "\f254";
+}
+.icon-hand-grab-o:before {
+	content: "\f255";
+}
+.icon-hand-rock-o:before {
+	content: "\f255";
+}
+.icon-hand-paper-o:before {
+	content: "\f256";
+}
+.icon-hand-stop-o:before {
+	content: "\f256";
+}
+.icon-hand-scissors-o:before {
+	content: "\f257";
+}
+.icon-hand-lizard-o:before {
+	content: "\f258";
+}
+.icon-hand-spock-o:before {
+	content: "\f259";
+}
+.icon-hand-pointer-o:before {
+	content: "\f25a";
+}
+.icon-hand-peace-o:before {
+	content: "\f25b";
+}
+.icon-trademark:before {
+	content: "\f25c";
+}
+.icon-registered:before {
+	content: "\f25d";
+}
+.icon-creative-commons:before {
+	content: "\f25e";
+}
+.icon-gg:before {
+	content: "\f260";
+}
+.icon-gg-circle:before {
+	content: "\f261";
+}
+.icon-tripadvisor:before {
+	content: "\f262";
+}
+.icon-odnoklassniki:before {
+	content: "\f263";
+}
+.icon-odnoklassniki-square:before {
+	content: "\f264";
+}
+.icon-get-pocket:before {
+	content: "\f265";
+}
+.icon-wikipedia-w:before {
+	content: "\f266";
+}
+.icon-safari:before {
+	content: "\f267";
+}
+.icon-chrome:before {
+	content: "\f268";
+}
+.icon-firefox:before {
+	content: "\f269";
+}
+.icon-opera:before {
+	content: "\f26a";
+}
+.icon-internet-explorer:before {
+	content: "\f26b";
+}
+.icon-television:before {
+	content: "\f26c";
+}
+.icon-tv:before {
+	content: "\f26c";
+}
+.icon-contao:before {
+	content: "\f26d";
+}
+.icon-500px:before {
+	content: "\f26e";
+}
+.icon-amazon:before {
+	content: "\f270";
+}
+.icon-calendar-plus-o:before {
+	content: "\f271";
+}
+.icon-calendar-minus-o:before {
+	content: "\f272";
+}
+.icon-calendar-times-o:before {
+	content: "\f273";
+}
+.icon-calendar-check-o:before {
+	content: "\f274";
+}
+.icon-industry:before {
+	content: "\f275";
+}
+.icon-map-pin:before {
+	content: "\f276";
+}
+.icon-map-signs:before {
+	content: "\f277";
+}
+.icon-map-o:before {
+	content: "\f278";
+}
+.icon-map:before {
+	content: "\f279";
+}
+.icon-commenting:before {
+	content: "\f27a";
+}
+.icon-commenting-o:before {
+	content: "\f27b";
+}
+.icon-houzz:before {
+	content: "\f27c";
+}
+.icon-vimeo:before {
+	content: "\f27d";
+}
+.icon-black-tie:before {
+	content: "\f27e";
+}
+.icon-fonticons:before {
+	content: "\f280";
+}
+.icon-eye2:before {
+	content: "\e000";
+}
+.icon-paper-clip:before {
+	content: "\e001";
+}
+.icon-mail2:before {
+	content: "\e002";
+}
+.icon-toggle:before {
+	content: "\e003";
+}
+.icon-layout:before {
+	content: "\e004";
+}
+.icon-link2:before {
+	content: "\e005";
+}
+.icon-bell2:before {
+	content: "\e006";
+}
+.icon-lock2:before {
+	content: "\e007";
+}
+.icon-unlock:before {
+	content: "\e008";
+}
+.icon-ribbon:before {
+	content: "\e009";
+}
+.icon-image2:before {
+	content: "\e010";
+}
+.icon-signal2:before {
+	content: "\e011";
+}
+.icon-target:before {
+	content: "\e012";
+}
+.icon-clipboard2:before {
+	content: "\e013";
+}
+.icon-clock2:before {
+	content: "\e014";
+}
+.icon-watch:before {
+	content: "\e015";
+}
+.icon-air-play:before {
+	content: "\e016";
+}
+.icon-camera2:before {
+	content: "\e017";
+}
+.icon-video2:before {
+	content: "\e018";
+}
+.icon-disc:before {
+	content: "\e019";
+}
+.icon-printer:before {
+	content: "\e020";
+}
+.icon-monitor:before {
+	content: "\e021";
+}
+.icon-server:before {
+	content: "\e022";
+}
+.icon-cog2:before {
+	content: "\e023";
+}
+.icon-heart2:before {
+	content: "\e024";
+}
+.icon-paragraph:before {
+	content: "\e025";
+}
+.icon-align-justify:before {
+	content: "\e026";
+}
+.icon-align-left2:before {
+	content: "\e027";
+}
+.icon-align-center:before {
+	content: "\e028";
+}
+.icon-align-right2:before {
+	content: "\e029";
+}
+.icon-book2:before {
+	content: "\e030";
+}
+.icon-layers2:before {
+	content: "\e031";
+}
+.icon-stack:before {
+	content: "\e032";
+}
+.icon-stack-2:before {
+	content: "\e033";
+}
+.icon-paper:before {
+	content: "\e034";
+}
+.icon-paper-stack:before {
+	content: "\e035";
+}
+.icon-search:before {
+	content: "\e036";
+}
+.icon-zoom-in:before {
+	content: "\e037";
+}
+.icon-zoom-out:before {
+	content: "\e038";
+}
+.icon-reply2:before {
+	content: "\e039";
+}
+.icon-circle-plus:before {
+	content: "\e040";
+}
+.icon-circle-minus:before {
+	content: "\e041";
+}
+.icon-circle-check:before {
+	content: "\e042";
+}
+.icon-circle-cross:before {
+	content: "\e043";
+}
+.icon-square-plus:before {
+	content: "\e044";
+}
+.icon-square-minus:before {
+	content: "\e045";
+}
+.icon-square-check:before {
+	content: "\e046";
+}
+.icon-square-cross:before {
+	content: "\e047";
+}
+.icon-microphone:before {
+	content: "\e048";
+}
+.icon-record:before {
+	content: "\e049";
+}
+.icon-skip-back:before {
+	content: "\e050";
+}
+.icon-rewind:before {
+	content: "\e051";
+}
+.icon-play:before {
+	content: "\e052";
+}
+.icon-pause:before {
+	content: "\e053";
+}
+.icon-stop:before {
+	content: "\e054";
+}
+.icon-fast-forward:before {
+	content: "\e055";
+}
+.icon-skip-forward:before {
+	content: "\e056";
+}
+.icon-shuffle2:before {
+	content: "\e057";
+}
+.icon-repeat:before {
+	content: "\e058";
+}
+.icon-folder2:before {
+	content: "\e059";
+}
+.icon-umbrella:before {
+	content: "\e060";
+}
+.icon-moon2:before {
+	content: "\e061";
+}
+.icon-thermometer2:before {
+	content: "\e062";
+}
+.icon-drop2:before {
+	content: "\e063";
+}
+.icon-sun:before {
+	content: "\e064";
+}
+.icon-cloud2:before {
+	content: "\e065";
+}
+.icon-cloud-upload:before {
+	content: "\e066";
+}
+.icon-cloud-download:before {
+	content: "\e067";
+}
+.icon-upload2:before {
+	content: "\e068";
+}
+.icon-download2:before {
+	content: "\e069";
+}
+.icon-location2:before {
+	content: "\e070";
+}
+.icon-location-2:before {
+	content: "\e071";
+}
+.icon-map2:before {
+	content: "\e072";
+}
+.icon-battery2:before {
+	content: "\e073";
+}
+.icon-head:before {
+	content: "\e074";
+}
+.icon-briefcase2:before {
+	content: "\e075";
+}
+.icon-speech-bubble:before {
+	content: "\e076";
+}
+.icon-anchor:before {
+	content: "\e077";
+}
+.icon-globe2:before {
+	content: "\e078";
+}
+.icon-box2:before {
+	content: "\e079";
+}
+.icon-reload:before {
+	content: "\e080";
+}
+.icon-share2:before {
+	content: "\e081";
+}
+.icon-marquee:before {
+	content: "\e082";
+}
+.icon-marquee-plus:before {
+	content: "\e083";
+}
+.icon-marquee-minus:before {
+	content: "\e084";
+}
+.icon-tag2:before {
+	content: "\e085";
+}
+.icon-power:before {
+	content: "\e086";
+}
+.icon-command:before {
+	content: "\e087";
+}
+.icon-alt:before {
+	content: "\e088";
+}
+.icon-esc:before {
+	content: "\e089";
+}
+.icon-bar-graph2:before {
+	content: "\e090";
+}
+.icon-bar-graph-2:before {
+	content: "\e091";
+}
+.icon-pie-graph:before {
+	content: "\e092";
+}
+.icon-star2:before {
+	content: "\e093";
+}
+.icon-arrow-left2:before {
+	content: "\e094";
+}
+.icon-arrow-right2:before {
+	content: "\e095";
+}
+.icon-arrow-up2:before {
+	content: "\e096";
+}
+.icon-arrow-down2:before {
+	content: "\e097";
+}
+.icon-volume:before {
+	content: "\e098";
+}
+.icon-mute:before {
+	content: "\e099";
+}
+.icon-content-right:before {
+	content: "\e100";
+}
+.icon-content-left:before {
+	content: "\e101";
+}
+.icon-grid2:before {
+	content: "\e102";
+}
+.icon-grid-2:before {
+	content: "\e103";
+}
+.icon-columns:before {
+	content: "\e104";
+}
+.icon-loader:before {
+	content: "\e105";
+}
+.icon-bag:before {
+	content: "\e106";
+}
+.icon-ban:before {
+	content: "\e107";
+}
+.icon-flag2:before {
+	content: "\e108";
+}
+.icon-trash2:before {
+	content: "\e109";
+}
+.icon-expand:before {
+	content: "\e110";
+}
+.icon-contract:before {
+	content: "\e111";
+}
+.icon-maximize:before {
+	content: "\e112";
+}
+.icon-minimize:before {
+	content: "\e113";
+}
+.icon-plus2:before {
+	content: "\e114";
+}
+.icon-minus2:before {
+	content: "\e115";
+}
+.icon-check2:before {
+	content: "\e116";
+}
+.icon-cross2:before {
+	content: "\e117";
+}
+.icon-move:before {
+	content: "\e118";
+}
+.icon-delete:before {
+	content: "\e119";
+}
+.icon-menu2:before {
+	content: "\e120";
+}
+.icon-archive2:before {
+	content: "\e121";
+}
+.icon-inbox2:before {
+	content: "\e122";
+}
+.icon-outbox:before {
+	content: "\e123";
+}
+.icon-file:before {
+	content: "\e124";
+}
+.icon-file-add:before {
+	content: "\e125";
+}
+.icon-file-subtract:before {
+	content: "\e126";
+}
+.icon-help2:before {
+	content: "\e127";
+}
+.icon-open:before {
+	content: "\e128";
+}
+.icon-ellipsis:before {
+	content: "\e129";
+}
+
diff --git a/website/themes/lektor-icon/assets/static/css/magnific-popup.css b/website/themes/lektor-icon/assets/static/css/magnific-popup.css
new file mode 100644
index 0000000..b0544c4
--- /dev/null
+++ b/website/themes/lektor-icon/assets/static/css/magnific-popup.css
@@ -0,0 +1,358 @@
+/* Magnific Popup CSS */
+
+.mfp-bg {
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 1042;
+  overflow: hidden;
+  position: fixed;
+  background: #0b0b0b;
+  opacity: 0.8;
+  filter: alpha(opacity=80); }
+
+.mfp-wrap {
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 1043;
+  position: fixed;
+  outline: none !important;
+  -webkit-backface-visibility: hidden;
+  backface-visibility: hidden; }
+
+.mfp-container {
+  text-align: center;
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  left: 0;
+  top: 0;
+  padding: 0 8px;
+  box-sizing: border-box; }
+
+.mfp-container:before {
+  content: '';
+  display: inline-block;
+  height: 100%;
+  vertical-align: middle; }
+
+.mfp-align-top .mfp-container:before {
+  display: none; }
+
+.mfp-content {
+  position: relative;
+  display: inline-block;
+  vertical-align: middle;
+  margin: 0 auto;
+  text-align: left;
+  z-index: 1045; }
+
+.mfp-inline-holder .mfp-content, .mfp-ajax-holder .mfp-content {
+  width: 100%;
+  cursor: auto; }
+
+.mfp-ajax-cur {
+  cursor: progress; }
+
+.mfp-zoom-out-cur, .mfp-zoom-out-cur .mfp-image-holder .mfp-close {
+  cursor: zoom-out; }
+
+.mfp-zoom {
+  cursor: pointer;
+  cursor: zoom-in; }
+
+.mfp-auto-cursor .mfp-content {
+  cursor: auto; }
+
+.mfp-close, .mfp-arrow, .mfp-preloader, .mfp-counter {
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none; }
+
+.mfp-loading.mfp-figure {
+  display: none; }
+
+.mfp-hide {
+  display: none !important; }
+
+.mfp-preloader {
+  color: #cccccc;
+  position: absolute;
+  top: 50%;
+  width: auto;
+  text-align: center;
+  margin-top: -0.8em;
+  left: 8px;
+  right: 8px;
+  z-index: 1044; }
+  .mfp-preloader a {
+    color: #cccccc; }
+    .mfp-preloader a:hover {
+      color: white; }
+
+.mfp-s-ready .mfp-preloader {
+  display: none; }
+
+.mfp-s-error .mfp-content {
+  display: none; }
+
+button.mfp-close, button.mfp-arrow {
+  overflow: visible;
+  cursor: pointer;
+  background: transparent;
+  border: 0;
+  -webkit-appearance: none;
+  -moz-appearance: none;
+  appearance: none;
+  display: block;
+  outline: none;
+  padding: 0;
+  z-index: 1046;
+  box-shadow: none; }
+button::-moz-focus-inner {
+  padding: 0;
+  border: 0; }
+
+.mfp-close {
+  width: 44px;
+  height: 44px;
+  line-height: 44px;
+  position: absolute;
+  right: 0;
+  top: 0;
+  text-decoration: none;
+  text-align: center;
+  opacity: 0.65;
+  filter: alpha(opacity=65);
+  padding: 0 0 18px 10px;
+  color: white;
+  font-style: normal;
+  font-size: 28px;
+  font-family: Raleway, "DejaVu Sans", "Open Sans", "Liberation Sans", Arial, sans-serif; }
+  .mfp-close:hover, .mfp-close:focus {
+    opacity: 1;
+    filter: alpha(opacity=100); }
+  .mfp-close:active {
+    top: 1px; }
+
+.mfp-close-btn-in .mfp-close {
+  color: #333333; }
+
+.mfp-image-holder .mfp-close, .mfp-iframe-holder .mfp-close {
+  color: white;
+  right: -6px;
+  text-align: right;
+  padding-right: 6px;
+  width: 100%; }
+
+.mfp-counter {
+  position: absolute;
+  top: 0;
+  right: 0;
+  color: #cccccc;
+  font-size: 12px;
+  line-height: 18px; }
+
+.mfp-arrow {
+  position: absolute;
+  opacity: 0.65;
+  filter: alpha(opacity=65);
+  margin: 0;
+  top: 50%;
+  margin-top: -55px;
+  padding: 0;
+  width: 90px;
+  height: 110px; }
+  .mfp-arrow:active {
+    margin-top: -54px; }
+  .mfp-arrow:hover, .mfp-arrow:focus {
+    opacity: 1;
+    filter: alpha(opacity=100); }
+  .mfp-arrow:before, .mfp-arrow:after, .mfp-arrow .mfp-b, .mfp-arrow .mfp-a {
+    content: '';
+    display: block;
+    width: 0;
+    height: 0;
+    position: absolute;
+    left: 0;
+    top: 0;
+    margin-top: 35px;
+    margin-left: 35px;
+    border: medium inset transparent; }
+  .mfp-arrow:after, .mfp-arrow .mfp-a {
+    border-top-width: 13px;
+    border-bottom-width: 13px;
+    top: 8px; }
+  .mfp-arrow:before, .mfp-arrow .mfp-b {
+    border-top-width: 21px;
+    border-bottom-width: 21px;
+    opacity: 0.7; }
+
+.mfp-arrow-left {
+  left: 0; }
+  .mfp-arrow-left:after, .mfp-arrow-left .mfp-a {
+    border-right: 17px solid white;
+    margin-left: 31px; }
+  .mfp-arrow-left:before, .mfp-arrow-left .mfp-b {
+    margin-left: 25px;
+    border-right: 27px solid #404040; }
+
+.mfp-arrow-right {
+  right: 0; }
+  .mfp-arrow-right:after, .mfp-arrow-right .mfp-a {
+    border-left: 17px solid white;
+    margin-left: 39px; }
+  .mfp-arrow-right:before, .mfp-arrow-right .mfp-b {
+    border-left: 27px solid #404040; }
+
+.mfp-iframe-holder {
+  padding-top: 40px;
+  padding-bottom: 40px; }
+  .mfp-iframe-holder .mfp-content {
+    line-height: 0;
+    width: 100%;
+    max-width: 900px; }
+  .mfp-iframe-holder .mfp-close {
+    top: -40px; }
+
+.mfp-iframe-scaler {
+  width: 100%;
+  height: 0;
+  overflow: hidden;
+  padding-top: 56.25%; }
+  .mfp-iframe-scaler iframe {
+    position: absolute;
+    display: block;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
+    background: black; }
+
+/* Main image in popup */
+img.mfp-img {
+  width: auto;
+  max-width: 100%;
+  height: auto;
+  display: block;
+  line-height: 0;
+  box-sizing: border-box;
+  padding: 40px 0 40px;
+  margin: 0 auto; }
+
+/* The shadow behind the image */
+.mfp-figure {
+  line-height: 0; }
+  .mfp-figure:after {
+    content: '';
+    position: absolute;
+    left: 0;
+    top: 40px;
+    bottom: 40px;
+    display: block;
+    right: 0;
+    width: auto;
+    height: auto;
+    z-index: -1;
+    box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
+    background: #404040; }
+  .mfp-figure small {
+    color: #bdbdbd;
+    display: block;
+    font-size: 12px;
+    line-height: 14px; }
+  .mfp-figure figure {
+    margin: 0; }
+
+.mfp-bottom-bar {
+  margin-top: -36px;
+  position: absolute;
+  top: 100%;
+  left: 0;
+  width: 100%;
+  cursor: auto; }
+
+.mfp-title {
+  text-align: left;
+  line-height: 18px;
+  color: #f5f5f5;
+  word-wrap: break-word;
+  padding-right: 36px; }
+
+.mfp-image-holder .mfp-content {
+  max-width: 100%; }
+
+.mfp-gallery .mfp-image-holder .mfp-figure {
+  cursor: pointer; }
+
+@media screen and (max-width: 800px) and (orientation: landscape), screen and (max-height: 300px) {
+  /**
+       * Remove all paddings around the image on small screen
+       */
+  .mfp-img-mobile .mfp-image-holder {
+    padding-left: 0;
+    padding-right: 0; }
+  .mfp-img-mobile img.mfp-img {
+    padding: 0; }
+  .mfp-img-mobile .mfp-figure:after {
+    top: 0;
+    bottom: 0; }
+  .mfp-img-mobile .mfp-figure small {
+    display: inline;
+    margin-left: 5px; }
+  .mfp-img-mobile .mfp-bottom-bar {
+    background: rgba(0, 0, 0, 0.6);
+    bottom: 0;
+    margin: 0;
+    top: auto;
+    padding: 3px 5px;
+    position: fixed;
+    box-sizing: border-box; }
+    .mfp-img-mobile .mfp-bottom-bar:empty {
+      padding: 0; }
+  .mfp-img-mobile .mfp-counter {
+    right: 5px;
+    top: 3px; }
+  .mfp-img-mobile .mfp-close {
+    top: 0;
+    right: 0;
+    width: 35px;
+    height: 35px;
+    line-height: 35px;
+    background: rgba(0, 0, 0, 0.6);
+    position: fixed;
+    text-align: center;
+    padding: 0; } }
+
+@media all and (max-width: 900px) {
+  .mfp-arrow {
+    transform: scale(0.75); }
+  .mfp-arrow-left {
+    transform-origin: 0; }
+  .mfp-arrow-right {
+    transform-origin: 100%; }
+  .mfp-container {
+    padding-left: 6px;
+    padding-right: 6px; } }
+
+.mfp-ie7 .mfp-img {
+  padding: 0; }
+.mfp-ie7 .mfp-bottom-bar {
+  width: 600px;
+  left: 50%;
+  margin-left: -300px;
+  margin-top: 5px;
+  padding-bottom: 5px; }
+.mfp-ie7 .mfp-container {
+  padding: 0; }
+.mfp-ie7 .mfp-content {
+  padding-top: 44px; }
+.mfp-ie7 .mfp-close {
+  top: 0;
+  right: 0;
+  padding-top: 0; }
diff --git a/website/themes/lektor-icon/assets/static/css/style.css b/website/themes/lektor-icon/assets/static/css/style.css
new file mode 100644
index 0000000..72efbe7
--- /dev/null
+++ b/website/themes/lektor-icon/assets/static/css/style.css
@@ -0,0 +1,2436 @@
+@charset "UTF-8";
+
+/*
+ * Lektor-Icon Theme
+ * Copyright (c) 2016- Lektor-Icon Contributors
+ *
+ * Original standalone HTML5 theme distributed under the terms of the
+ * Creative Commons Attribution 3.0 license -->
+ * https://creativecommons.org/licenses/by/3.0/
+ *
+ * Additions, modifications and porting released under the terms of the
+ * MIT (Expat) License: https://opensource.org/licenses/MIT
+ * See the LICENSE.txt file for more details
+ * https://github.com/spyder-ide/lektor-icon/blob/master/LICENSE.txt
+ *
+ * For information on the included third-party assets, see NOTICE.txt
+ * https://github.com/spyder-ide/lektor-icon/blob/master/NOTICE.txt
+ */
+
+html {
+  font-size: 100%;
+  font-family: sans-serif;
+  line-height: 1.15;
+  box-sizing: border-box;
+  -ms-text-size-adjust: 100%;
+  -webkit-text-size-adjust: 100%;
+  text-size-adjust: 100%;
+}
+
+@font-face {
+  font-family: 'icomoon';
+  src: url("../fonts/icomoon/icomoon.woff?srf3rx") format("woff");
+  font-weight: normal;
+  font-style: normal;
+}
+
+[class^="icon-"], [class*=" icon-"] {
+  font-family: 'icomoon';
+  speak: none;
+  font-style: normal;
+  font-weight: normal;
+  font-variant: normal;
+  text-transform: none;
+  line-height: 1;
+}
+
+.icon-play2:before {
+  content: "\f04b";
+}
+.icon-globe2:before {
+  content: "\e078";
+}
+.icon-facebook:before {
+  content: "\f09a";
+}
+.icon-twitter:before {
+  content: "\f099";
+}
+.icon-dribbble:before {
+  content: "\f17d";
+}
+.icon-instagram:before {
+  content: "\f16d";
+}
+.icon-github:before {
+  content: "\f09b";
+}
+.icon-linkedin:before {
+  content: "\f0e1";
+}
+
+.pull-left {
+  float: left !important;
+}
+
+.pull-right {
+  float: right !important;
+}
+
+/* small screen portrait */
+
+@media screen and (max-width: 480px) {
+
+  .pull-right {
+    float: none!important;
+  }
+
+}
+
+/* small screen landscape */
+
+@media screen and (max-width: 768px) {
+
+  .pull-right {
+    float: none!important;
+  }
+
+}
+
+*,
+:after,
+:before {
+  box-sizing: border-box;
+}
+
+body {
+  font-family: Raleway, "DejaVu Sans", "Open Sans", "Liberation Sans", Helvetica, Arial, sans-serif;
+  font-size: 18px;
+  font-weight: 300;
+  line-height: 1.9;
+  color: #505050;
+  margin: 0;
+  padding: 0;
+  background: #ffffff;
+  background-color: #ffffff;
+}
+body.fh5co-overflow {
+  overflow-x: hidden;
+  width: 100%;
+}
+
+article,
+aside,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+menu,
+nav,
+section,
+summary {
+  display: block;
+}
+
+a {
+  color: #ee1c24;
+  color: var(--theme-accent-color, #ee1c24);
+  transition: 0.5s;
+  line-height: inherit;
+  cursor: pointer;
+  background-color: transparent;
+  text-decoration: none;
+  -webkit-text-decoration-skip: objects;
+  text-decoration-skip: objects;
+}
+a:hover,
+a:focus,
+a:active {
+  color: #ee1c24;
+  color: var(--theme-accent-color, #ee1c24);
+}
+a:hover,
+a:active {
+  outline: 0;
+  outline-width: 0;
+}
+a:hover,
+a:focus {
+  text-decoration: underline;
+}
+a:focus {
+  outline: thin dotted;
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
+
+h1, h2, h3, h4, h5, h6,
+.h1, .h2, .h3, .h4, .h5, .h6 {
+  font-family: Raleway, "DejaVu Sans", "Open Sans", "Liberation Sans", Helvetica, Arial, sans-serif;
+  font-weight: 500;
+  line-height: 1.1;
+  color: #000000;
+  margin: 0;
+  margin-bottom: 30px;
+}
+
+p {
+  margin: 0 0 10px;
+  margin-bottom: 1rem;
+  font-size: inherit;
+  line-height: 1.9;
+  text-rendering: optimizeLegibility;
+}
+
+img {
+  border: 0;
+  border-style: none;
+  display: inline-block;
+  vertical-align: middle;
+  max-width: 100%;
+  height: auto;
+}
+
+.img-responsive {
+  display: block;
+}
+
+.img-rounded {
+  border-radius: 6px;
+}
+
+.img-circle {
+  border-radius: 50%;
+}
+
+.container {
+  margin-right: auto;
+  margin-left: auto;
+  padding-left: 15px;
+  padding-right: 15px;
+}
+.container:before,
+.container:after {
+  content: " ";
+  display: table;
+}
+.container:after {
+  clear: both;
+}
+.fh5co-2col-inner {
+  width: 85%;
+}
+
+@media screen and (min-width: 769px) {
+  .container {
+    width: 768px;
+  }
+  .fh5co-2col-inner {
+    width: 384px;
+  }
+}
+@media screen and (min-width: 900px) {
+  .container {
+    width: 900px;
+  }
+  .fh5co-2col-inner {
+    width: 450px;
+  }
+}
+@media screen and (min-width: 1024px) {
+  .container {
+    width: 1024px;
+  }
+  .fh5co-2col-inner {
+    width: 512px;
+  }
+}
+@media screen and (min-width: 1200px) {
+  .container {
+    width: 1200px;
+  }
+  .fh5co-2col-inner {
+    width: 600px;
+  }
+}
+@media screen and (min-width: 1366px) {
+  .container {
+    width: 1366px;
+  }
+  .fh5co-2col-inner {
+    width: 683px;
+  }
+}
+@media screen and (min-width: 2500px) {
+  .container {
+    width: 1600px;
+  }
+  .fh5co-2col-inner {
+    width: 800px;
+  }
+}
+
+.container-fluid {
+  margin-right: auto;
+  margin-left: auto;
+  padding-left: 15px;
+  padding-right: 15px;
+}
+.container-fluid:before,
+.container-fluid:after {
+  content: " ";
+  display: table;
+}
+.container-fluid:after {
+  clear: both;
+}
+
+.dropup,
+.dropdown {
+  position: relative;
+}
+
+.btn {
+  margin-right: 4px;
+  margin-bottom: 4px;
+  font-family: Raleway, "DejaVu Sans", "Open Sans", "Liberation Sans", Helvetica, Arial, sans-serif;
+  font-size: 12px;
+  letter-spacing: 2px;
+  text-transform: uppercase;
+  font-weight: 700;
+  border-radius: 100px;
+  transition: 0.5s;
+}
+.btn.btn-md {
+  padding: 10px 20px !important;
+}
+.btn.btn-lg {
+  padding: 18px 36px !important;
+}
+.btn:hover,
+.btn:active,
+.btn:focus {
+  box-shadow: none !important;
+  outline: none !important;
+}
+
+.btn-primary {
+  background: #ee1c24;
+  background: var(--theme-accent-color, #ee1c24);
+  color: #ffffff;
+  border: 2px solid #ee1c24;
+  border: 2px solid var(--theme-accent-color, #ee1c24);
+}
+.btn-primary:hover,
+.btn-primary:focus,
+.btn-primary:active {
+  background: #ee1c24 !important;
+  background: var(--theme-accent-color, #ee1c24) !important;
+  border-color: #ee1c24 !important;
+  border-color: var(--theme-accent-color, #ee1c24) !important;
+}
+.btn-primary.btn-outline {
+  background: transparent;
+  color: #ee1c24;
+  color: var(--theme-accent-color, #ee1c24);
+  border: 2px solid #ee1c24;
+  border: 2px solid var(--theme-accent-color, #ee1c24);
+}
+.btn-primary.btn-outline:hover,
+.btn-primary.btn-outline:focus,
+.btn-primary.btn-outline:active {
+  background: #ee1c24;
+  background: var(--theme-accent-color, #ee1c24);
+  color: #ffffff;
+}
+
+.btn-success {
+  background: #ee1c24;
+  background: var(--theme-accent-color, #ee1c24);
+  color: #ffffff;
+  border: 2px solid #ee1c24;
+  border: 2px solid var(--theme-accent-color, #ee1c24);
+}
+.btn-success:hover,
+.btn-success:focus,
+.btn-success:active {
+  background: #ee1c24 !important;
+  background: var(--theme-accent-color, #ee1c24) !important;
+  border-color: #ee1c24 !important;
+  border-color: var(--theme-accent-color, #ee1c24) !important;
+}
+.btn-success.btn-outline {
+  background: transparent;
+  color: #ee1c24;
+  color: var(--theme-accent-color, #ee1c24);
+  border: 2px solid #ee1c24;
+  border: 2px solid var(--theme-accent-color, #ee1c24);
+}
+.btn-success.btn-outline:hover,
+.btn-success.btn-outline:focus,
+.btn-success.btn-outline:active {
+  background: #ee1c24;
+  background: var(--theme-accent-color, #ee1c24);
+  color: #ffffff;
+}
+
+.btn-info {
+  background: #1784fb;
+  color: #ffffff;
+  border: 2px solid #1784fb;
+}
+.btn-info:hover,
+.btn-info:focus,
+.btn-info:active {
+  background: #0477f4 !important;
+  border-color: #0477f4 !important;
+}
+.btn-info.btn-outline {
+  background: transparent;
+  color: #1784fb;
+  border: 2px solid #1784fb;
+}
+.btn-info.btn-outline:hover,
+.btn-info.btn-outline:focus,
+.btn-info.btn-outline:active {
+  background: #1784fb;
+  color: #ffffff;
+}
+
+.btn-warning {
+  background: #fed330;
+  color: #ffffff;
+  border: 2px solid #fed330;
+}
+.btn-warning:hover,
+.btn-warning:focus,
+.btn-warning:active {
+  background: #fed330 !important;
+  border-color: #fed330 !important;
+}
+.btn-warning.btn-outline {
+  background: transparent;
+  color: #fed330;
+  border: 2px solid #fed330;
+}
+.btn-warning.btn-outline:hover,
+.btn-warning.btn-outline:focus,
+.btn-warning.btn-outline:active {
+  background: #fed330;
+  color: #ffffff;
+}
+
+.btn-danger {
+  background: #fb4f59;
+  color: #ffffff;
+  border: 2px solid #fb4f59;
+}
+.btn-danger:hover, .btn-danger:focus,
+.btn-danger:active {
+  background: #fa3641 !important;
+  border-color: #fa3641 !important;
+}
+.btn-danger.btn-outline {
+  background: transparent;
+  color: #fb4f59;
+  border: 2px solid #fb4f59;
+}
+.btn-danger.btn-outline:hover,
+.btn-danger.btn-outline:focus,
+.btn-danger.btn-outline:active {
+  background: #fb4f59;
+  color: #ffffff;
+}
+
+.btn-outline {
+  background: none;
+  border: 2px solid #888888;
+  font-size: 12px;
+  letter-spacing: 2px;
+  text-transform: uppercase;
+  transition: 0.3s;
+}
+.btn-outline:hover,
+.btn-outline:focus,
+.btn-outline:active {
+  box-shadow: none;
+}
+
+.form-control {
+  box-shadow: none;
+  background: transparent;
+  border: 2px solid rgba(0, 0, 0, 0.1);
+  height: 54px;
+  font-size: 18px;
+  font-weight: 300;
+}
+.form-control:active,
+.form-control:focus {
+  outline: none;
+  box-shadow: none;
+  border-color: #ee1c24;
+  border-color: var(--theme-accent-color, #ee1c24);
+}
+
+.fh5co-page {
+  overflow-x: hidden;
+  position: relative;
+  z-index: 1;
+}
+
+.hero-section {
+  min-height: 300px;
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: center center;
+  box-shadow: inset -1px -11px 21px -15px rgba(0, 0, 0, 0.75);
+  position: relative;
+}
+.hero-section > .container {
+  position: relative;
+  z-index: 2;
+}
+.hero-section .fh5co-copy {
+  display: table;
+  width: 100%;
+}
+.hero-section .fh5co-copy-inner {
+  display: table-cell;
+  width: 100%;
+  vertical-align: middle;
+}
+.hero-section .fh5co-copy-inner h1,
+.hero-section .fh5co-copy-inner h2 {
+  margin: 0;
+  padding: 0;
+}
+.hero-section .fh5co-copy-inner h1 {
+  color: #ffffff;
+  font-size: 55px;
+  margin-bottom: 20px;
+  line-height: 1.2;
+  font-weight: 300;
+}
+.hero-section .fh5co-copy-inner h2 {
+  font-size: 20px;
+  color: #ffffff;
+  line-height: 1.9;
+  color: rgba(255, 255, 255, 0.7);
+}
+.hero-section .fh5co-copy-inner a {
+  color: #ffffff;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.4);
+}
+.hero-section .fh5co-copy-inner a:hover {
+  border-bottom: 1px solid rgba(255, 255, 255, 0.9);
+  text-decoration: none;
+}
+
+.fh5co-main-nav {
+  position: relative;
+  background: #ffffff;
+  font-size: 11px;
+}
+.fh5co-main-nav.fh5co-shadow {
+  box-shadow: 0 0 7px 0 rgba(0, 0, 0, 0.6);
+}
+.fh5co-main-nav .fh5co-menu-1 {
+  align-items: center;
+  display: flex;
+  float: left;
+  line-height: 0;
+  vertical-align: middle;
+  width: 100%;
+}
+
+.fh5co-main-nav .fh5co-menu-1 .pull-left {
+  flex-shrink: 0;
+  margin-left: 2em;
+  min-height: 55px;
+}
+
+.fh5co-main-nav .fh5co-menu-1 .main-nav-container {
+  margin-left: auto;
+  margin-right: 0.5em;
+}
+
+.fh5co-main-nav .fh5co-menu-1 .side-nav-container {
+  float: right;
+  margin-left: 3em;
+}
+
+.fh5co-main-nav .fh5co-menu-1 a {
+  padding: 1em 0;
+  margin-right: 2em;
+  color: #707070;
+  text-transform: uppercase;
+  letter-spacing: 2px;
+  font-size: 13px;
+  font-weight: 400;
+  display: inline-block;
+  vertical-align: middle;
+}
+.fh5co-main-nav .fh5co-menu-1 a:hover,
+.fh5co-main-nav .fh5co-menu-1 a:focus,
+.fh5co-main-nav .fh5co-menu-1 a:active {
+  outline: none;
+  text-decoration: none;
+}
+.fh5co-main-nav .fh5co-menu-1 a.active {
+  font-weight: 400;
+  color: #000000;
+}
+
+.fh5co-main-nav .fh5co-logo {
+  text-align: center;
+  width: 19.33%;
+  font-size: 40px;
+  font-family: Raleway, "DejaVu Sans", "Open Sans", "Liberation Sans", Helvetica, Arial, sans-serif;
+  font-weight: 700;
+  font-style: italic;
+}
+.fh5co-main-nav .fh5co-logo a {
+  position: relative;
+  top: -5px;
+  display: inline-block;
+}
+.fh5co-main-nav .fh5co-menu-2 {
+  text-align: left;
+  width: 40.33%;
+}
+
+.fh5co-heading .heading {
+  font-size: 50px;
+  font-style: italic;
+  position: relative;
+  padding-bottom: 30px;
+  margin-bottom: 30px;
+}
+@media screen and (max-width: 768px) {
+  .fh5co-heading .heading {
+    font-size: 30px;
+  }
+}
+.fh5co-heading .heading:after {
+  content: "";
+  position: absolute;
+  bottom: 0;
+  width: 40px;
+  height: 2px;
+  left: 50%;
+  background: #fb6e14;
+  margin-left: -20px;
+}
+.fh5co-heading .sub-heading {
+  font-size: 20px;
+  line-height: 1.5;
+}
+@media screen and (max-width: 768px) {
+  .fh5co-heading .sub-heading {
+    font-size: 18px;
+  }
+}
+
+.fh5co-heading {
+  margin-left: auto;
+  margin-right: auto;
+  margin-bottom: 1em;
+  width: 80%;
+}
+.fh5co-heading h2 {
+  font-size: 2.1em;
+  font-weight: 500;
+  color: #333333;
+  padding-bottom: 20px;
+  margin-bottom: 20px;
+  position: relative;
+}
+.fh5co-heading h2:after,
+.rule-under-heading {
+  position: absolute;
+  content: "";
+  width: 50px;
+  height: 2px;
+  bottom: 0;
+  left: 50%;
+  margin-left: -25px;
+  background: #ee1c24;
+  background: var(--theme-accent-color, #ee1c24);
+}
+.fh5co-heading p {
+  font-size: 18px;
+}
+
+.body-section {
+  clear: both;
+  padding-top: 3em;
+  padding-bottom: 4em;
+  position: relative;
+}
+
+.light-bg-section {
+  background: #ffffff;
+}
+
+.dark-bg-section {
+  background: #e8ecf1;
+}
+
+.mission-section {
+  display: flex;
+  flex-wrap: wrap;
+  padding-bottom: 0;
+  width: 100%;
+}
+
+.mission-section .fh5co-2col {
+  width: 50%;
+}
+@media screen and (max-width: 768px) {
+  .mission-section .fh5co-heading {
+    margin-bottom: 0;
+  }
+  .mission-section .fh5co-2col {
+    width: 100%;
+  }
+  .mission-section .fh5co-2col.right {
+    height: 480px;
+  }
+}
+@media screen and (max-width: 768px) and (max-height: 768px) {
+  .mission-section .fh5co-2col.right {
+    height: 360px;
+  }
+}
+@media screen and (max-width: 768px) and (max-height: 480px) {
+  .mission-section .fh5co-2col.right {
+    height: 240px;
+  }
+}
+
+.mission-section .fh5co-2col-inner {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 480px;
+  margin-right: auto;
+  margin-left: auto;
+  padding-left: 48px;
+  padding-right: 30px;
+}
+
+@media screen and (max-width: 768px) {
+  .mission-section .fh5co-2col-inner {
+    display: block;
+    min-height: unset;
+    width: 80% !important;
+    margin-left: auto;
+    margin-right: auto;
+    padding: 0;
+  }
+}
+
+.mission-section .fh5co-2col-inner.left {
+  float: right;
+}
+.mission-section .fh5co-2col-inner.right {
+  float: left;
+}
+@media screen and (max-width: 768px) {
+  .mission-section .fh5co-2col-inner.left,
+  .mission-section .fh5co-2col-inner.right {
+    float: none;
+  }
+}
+
+.mission-image-container {
+  padding-left: 5%;
+  width: 100%;
+  height: 100%;
+}
+@media screen and (max-width: 768px) {
+  .mission-image-container {
+  padding: 0;
+  width: 100%;
+  height: 100%;
+  }
+}
+
+.mission-image {
+  background-color: #ee1c24;
+  background-color: var(--theme-accent-color, #ee1c24);
+  background-position: center;
+  background-repeat: no-repeat;
+  background-size: cover;
+  width: 100%;
+  height: 100%;
+}
+
+.fh5co-tabs-container {
+  flex-grow: 1;
+  padding-top: 1em;
+  padding-bottom: 2em;
+  position: relative;
+}
+@media screen and (max-width: 768px) {
+  .fh5co-tabs-container {
+    padding-bottom: 1.5em;
+  }
+  .notitle-section .fh5co-tabs-container {
+    padding-top: 3em;
+  }
+}
+@media screen and (max-width: 480px) {
+  .fh5co-tabs-container {
+    padding-top: 0.5em;
+  }
+}
+
+.fh5co-tabs-container .fh5co-tabs {
+  display: flex;
+  float: left;
+  justify-content: space-around;
+  padding: 0;
+  margin: 0 0 30px 0;
+  width: 100%;
+}
+.fh5co-tabs-container .fh5co-tabs.fh5co-two li {
+  width: 50%;
+}
+.fh5co-tabs-container .fh5co-tabs.fh5co-three li {
+  width: 33.33%;
+}
+.fh5co-tabs-container .fh5co-tabs.fh5co-four li {
+  width: 25%;
+}
+.fh5co-tabs-container .fh5co-tabs li {
+  display: inline-block;
+  flex-grow: 1;
+  float: left;
+  padding: 0;
+  margin: 0;
+  list-style: none;
+  text-align: center;
+}
+.fh5co-tabs-container .fh5co-tabs li a {
+  border-bottom: 1px solid #cccccc;
+  padding-top: 0.5em;
+  padding-bottom: 1.5em;
+  float: left;
+  width: 100%;
+  display: block;
+  letter-spacing: 2px;
+  font-size: 13px;
+  text-transform: uppercase;
+  font-weight: 700;
+  color: #888888;
+}
+.fh5co-tabs-container .fh5co-tabs li a:hover,
+.fh5co-tabs-container .fh5co-tabs li a:active,
+.fh5co-tabs-container .fh5co-tabs li a:focus {
+  text-decoration: none;
+  outline: none;
+}
+.fh5co-tabs-container .fh5co-tabs li.active a {
+  color: #ee1c24;
+  color: var(--theme-accent-color, #ee1c24);
+  border-bottom: 1px solid #ee1c24;
+  border-bottom: 1px solid var(--theme-accent-color, #ee1c24);
+}
+@media screen and (max-width: 480px) {
+  .fh5co-tabs-container .fh5co-tabs {
+    display: block;
+  }
+  .fh5co-tabs-container .fh5co-tabs li {
+    width: 100% !important;
+  }
+  .fh5co-tabs-container .fh5co-tabs li a {
+    padding-top: 1em;
+    padding-bottom: 0.5em;
+  }
+}
+.fh5co-tabs-container .fh5co-tab-content,
+.mission-image {
+  display: none;
+  transition: 0.5s;
+}
+.fh5co-tabs-container .fh5co-tab-content h2 {
+  font-size: 1.2em;
+  font-weight: 400;
+  line-height: 1.6;
+  margin-bottom: 30px;
+  color: #707070;
+}
+.fh5co-tabs-container .fh5co-tab-content.active,
+.mission-image.active {
+  display: block;
+}
+
+.mission-1col-container {
+  width: 100%;
+}
+
+.fh5co-grid {
+  padding-left: 2em;
+}
+@media screen and (max-width: 768px) {
+  .fh5co-grid {
+    padding-left: 0em;
+  }
+}
+.fh5co-grid .fh5co-grid-item {
+  width: 50%;
+  float: left;
+  background-size: cover;
+  background-position: center center;
+  background-repeat: no-repeat;
+}
+@media screen and (max-width: 768px) {
+  .fh5co-grid .fh5co-grid-item {
+    height: 300px !important;
+  }
+}
+@media screen and (max-width: 480px) {
+  .fh5co-grid .fh5co-grid-item {
+    width: 100%;
+    height: 200px !important;
+  }
+}
+
+.gototop {
+  position: fixed;
+  bottom: 20px;
+  right: 20px;
+  z-index: 999;
+  opacity: 0;
+  visibility: hidden;
+  transition: 0.5s;
+}
+.gototop.active {
+  opacity: 1;
+  visibility: visible;
+}
+.gototop a {
+  width: 50px;
+  height: 50px;
+  display: table;
+  background: rgba(0, 0, 0, 0.5);
+  color: #ffffff;
+  text-align: center;
+  border-radius: 4px;
+}
+.gototop a i {
+  height: 50px;
+  display: table-cell;
+  vertical-align: middle;
+}
+.gototop a:hover,
+.gototop a:active,
+.gototop a:focus {
+  text-decoration: none;
+  outline: none;
+}
+
+.services-section .col-md-4,
+.team-section .col-md-4 {
+  padding-left: 10px;
+  padding-right: 10px;
+}
+
+@media screen and (max-width: 768px) {
+  .services-section {
+    padding: 5em 0 3em 0;
+  }
+}
+.services-section .fh5co-video {
+  position: absolute;
+  width: 90px;
+  height: 90px;
+  left: 50%;
+  margin-left: -45px;
+  top: -45px;
+  text-align: center;
+}
+
+.services-section .video-spacer {
+  margin-bottom: 3em;
+}
+
+.services-section .fh5co-video span {
+  display: block;
+  padding-top: 100px;
+  font-size: 14px;
+}
+.services-section .fh5co-video a {
+  float: left;
+  width: 90px;
+  height: 90px;
+  background: #ee1c24;
+  background: var(--theme-accent-color, #ee1c24);
+  color: #ffffff;
+  border-radius: 50%;
+  display: table;
+  box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
+}
+.services-section .fh5co-video a i {
+  padding-left: 10px;
+  text-align: center;
+  height: 90px;
+  display: table-cell;
+  vertical-align: middle;
+  font-size: 40px;
+  line-height: 40px;
+}
+.services-section .fh5co-video a:hover {
+  background: #ee1c24;
+  background: var(--theme-accent-color, #ee1c24);
+}
+.services-section .container .col-md-6 .text-center .fh5co-heading {
+  margin-top: -45px;
+}
+.services-section .fh5co-video a:hover,
+.services-section .fh5co-video a:focus,
+.services-section .fh5co-video a:active {
+  text-decoration: none;
+  outline: none;
+  transform: scale(1.2);
+}
+.services-section .service {
+  text-align: center;
+  padding: 2em;
+  margin-top: 10px;
+  margin-bottom: 10px;
+  background: #ffffff;
+  border: 1px solid transparent;
+  border-radius: 5px;
+  transition: 0.5s;
+}
+.services-section .service .icon {
+  text-align: center;
+  width: 100%;
+}
+.services-section .service .icon i {
+  font-size: 40px;
+  color: #ee1c24;
+  color: var(--theme-accent-color, #ee1c24);
+}
+.services-section .service h3 {
+  text-align: center;
+  font-size: 1.4em;
+  font-weight: 500;
+  color: #ee1c24;
+  color: var(--theme-accent-color, #ee1c24);
+}
+.services-section .service:hover,
+.services-section .service:focus {
+  border: 1px solid #e6e6e6;
+  box-shadow: 0 0 7px 0 rgba(0, 0, 0, 0.1);
+}
+
+.checked {
+  padding: 0;
+  margin: 0 0 30px 0;
+}
+.checked li {
+  position: relative;
+  list-style: none;
+  padding-left: 40px;
+}
+.checked li:before {
+  position: absolute;
+  left: 0;
+  top: .15em;
+  font-size: 23px;
+  color: #ee1c24;
+  color: var(--theme-accent-color, #ee1c24);
+  font-family: 'icomoon';
+  display: none;
+  font-style: normal;
+  font-weight: normal;
+  font-variant: normal;
+  text-transform: none;
+  line-height: 1;
+  content: "\e116";
+}
+
+@media screen and (max-width: 768px) {
+  .team-section {
+    padding: 3em 0;
+  }
+}
+.team-section .person {
+  padding: 2em;
+  margin-top: 10px;
+  margin-bottom: 10px;
+  text-align: center;
+  transition: 0.5s;
+  border-radius: 5px;
+  background: #ffffff;
+}
+.team-section .person img {
+  width: 50%;
+  margin: 0 auto 30px auto;
+  border-radius: 50%;
+}
+.team-section .person h3 {
+  font-size: 1.25em;
+  margin-bottom: 0.5em;
+}
+.team-section .person h4 {
+  color: #707070;
+  font-size: 1em;
+  margin-bottom: 0.5em;
+}
+.team-section .person .social {
+  padding: 0;
+  margin: 0;
+}
+.team-section .person .social li {
+  padding: 0;
+  margin: 0;
+  list-style: none;
+  display: inline-block;
+}
+.team-section .person .social li a {
+  padding: 10px;
+}
+.team-section .person .social li a:hover {
+  text-decoration: none;
+}
+.team-section .person:focus,
+.team-section .person:hover {
+  background: #ffffff;
+  box-shadow: 0 0 7px 0 rgba(0, 0, 0, 0.1);
+}
+
+.gallery-section {
+  float: left;
+  padding-bottom: 0;
+  width: 100%;
+}
+
+.gallery-section .fh5co-gallery-header {
+  margin-bottom: 1.5em;
+}
+
+.gallery-section .gallery-item-headers {
+  padding-top: 1em;
+}
+
+.gallery-section .fh5co-item {
+  display: block;
+  position: relative;
+  width: 25%;
+  z-index: 2;
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: center center;
+  height: 300px;
+  float: left;
+  transition: 0.5s;
+  border-style: solid;
+  border-width: 1px;
+  font-size: 18px;
+}
+
+.gallery-section .gallery-item-title {
+  margin-top: 3.5em;
+}
+
+.gallery-section .fh5co-item-link {
+  display: block;
+  position: relative;
+  width: 25%;
+  z-index: 2;
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: center center;
+  float: left;
+  transition: 0.5s;
+  color: #ee1c24;
+  color: var(--theme-accent-color, #ee1c24);
+}
+
+.gallery-section .fh5co-item h3 {
+  position: relative;
+  top: -1.4em;
+  text-align: center;
+  align-content: center;
+  font-size: 1.4em;
+  font-family: Raleway, "DejaVu Sans", "Open Sans", "Liberation Sans", Helvetica, Arial, sans-serif;
+  color: #ee1c24;
+  color: var(--theme-accent-color, #ee1c24);
+  z-index: 5;
+  margin: 0;
+}
+
+.gallery-section .fh5co-item .fh5co-overlay {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  right: 0;
+  left: 0;
+  z-index: 0;
+  background: #ee1c24;
+  background: var(--theme-accent-color, #ee1c24);
+  opacity: 0;
+  visibility: hidden;
+  transition: 0.1s;
+}
+.gallery-section .fh5co-item .fh5co-copy {
+  z-index: 3;
+  top: 0;
+  bottom: 0;
+  right: 0;
+  left: 0;
+  position: absolute;
+  height: 300px;
+  display: table;
+  text-align: center;
+  width: 100%;
+}
+.gallery-section .fh5co-item .fh5co-copy > .fh5co-copy-inner {
+  width: 100%;
+  height: 300px;
+  display: table-cell;
+  vertical-align: middle;
+}
+
+.gallery-section .fh5co-item .fh5co-copy > .fh5co-copy-inner h2,
+.gallery-section .fh5co-item .fh5co-copy > .fh5co-copy-inner h3 {
+  margin: 0;
+  padding: 0;
+  color: #ffffff;
+  opacity: 0;
+  visibility: hidden;
+  position: relative;
+  transition: 0.3s;
+}
+.gallery-section .fh5co-item .fh5co-copy > .fh5co-copy-inner h2 {
+  top: -10px;
+  font-size: 20px;
+  margin-bottom: 10px;
+}
+.gallery-section .fh5co-item .fh5co-copy > .fh5co-copy-inner h3 {
+  top: 0px;
+  font-size: 16px;
+  color: rgba(255, 255, 255, 0.7);
+}
+
+@media screen and (max-width: 1200px) {
+  .gallery-section .fh5co-item {
+    width: 50%;
+  }
+}
+@media screen and (max-width: 768px) {
+  .gallery-section .fh5co-item {
+    width: 100%;
+  }
+  .gallery-section .fh5co-item,
+  .gallery-section .fh5co-item .fh5co-copy,
+  .gallery-section .fh5co-item .fh5co-copy .fh5co-copy-inner {
+    height: 400px;
+  }
+}
+@media screen and (max-width: 768px) and (max-height: 768px) {
+  .gallery-section .fh5co-item,
+  .gallery-section .fh5co-item .fh5co-copy,
+  .gallery-section .fh5co-item .fh5co-copy .fh5co-copy-inner {
+    height: 300px;
+  }
+}
+@media screen and (max-width: 768px) and (max-height: 768px) {
+  .gallery-section .fh5co-item,
+  .gallery-section .fh5co-item .fh5co-copy,
+  .gallery-section .fh5co-item .fh5co-copy .fh5co-copy-inner {
+    height: 200px;
+  }
+}
+@media screen and (max-width: 768px) and (max-height: 480px) {
+  .mission-section .fh5co-2col.right {
+    height: 150px;
+  }
+}
+
+.gallery-section .fh5co-item:hover,
+.gallery-section .fh5co-item:focus,
+.gallery-section .fh5co-item:active {
+  background-size: cover;
+  text-decoration: none;
+}
+.gallery-section .fh5co-item:hover .fh5co-overlay,
+.gallery-section .fh5co-item:focus .fh5co-overlay,
+.gallery-section .fh5co-item:active .fh5co-overlay {
+  opacity: .8;
+  visibility: visible;
+}
+.gallery-section .fh5co-item:hover .fh5co-copy-inner h2,
+.gallery-section .fh5co-item:hover .fh5co-copy-inner h3,
+.gallery-section .fh5co-item:focus .fh5co-copy-inner h2,
+.gallery-section .fh5co-item:focus .fh5co-copy-inner h3,
+.gallery-section .fh5co-item:active .fh5co-copy-inner h2,
+.gallery-section .fh5co-item:active .fh5co-copy-inner h3 {
+  opacity: 1;
+  visibility: visible;
+}
+.gallery-section .fh5co-item:hover .fh5co-copy-inner h2,
+.gallery-section .fh5co-item:focus .fh5co-copy-inner h2,
+.gallery-section .fh5co-item:active .fh5co-copy-inner h2 {
+  top: 0;
+}
+.gallery-section .fh5co-item:hover .fh5co-copy-inner h3,
+.gallery-section .fh5co-item:focus .fh5co-copy-inner h3,
+.gallery-section .fh5co-item:active .fh5co-copy-inner h3 {
+  bottom: 0;
+}
+
+#fh5co-footer {
+  clear: both;
+  padding: 3em 0;
+  background: #404040;
+  box-shadow: inset 0px 10px 21px -15px rgba(0, 0, 0, 0.1);
+}
+@media screen and (max-width: 768px) {
+  #fh5co-footer {
+    padding: 3em 0;
+  }
+}
+#fh5co-footer p:last-child {
+  margin-bottom: 0;
+}
+#fh5co-footer .fh5co-social {
+  float: right;
+  text-align: left;
+}
+@media screen and (max-width: 768px) {
+  #fh5co-footer .fh5co-social {
+    float: left;
+    text-align: left;
+  }
+}
+#fh5co-footer .fh5co-social li {
+  display: inline-block;
+}
+#fh5co-footer .fh5co-social li a {
+  padding: 0 10px;
+}
+#fh5co-footer .fh5co-social li a i {
+  font-size: 50px;
+}
+#fh5co-footer .fh5co-social li a:hover,
+#fh5co-footer .fh5co-social li a:focus,
+#fh5co-footer .fh5co-social li a:active {
+  text-decoration: none;
+  outline: none;
+}
+
+.mfp-with-zoom .mfp-container,
+.mfp-with-zoom.mfp-bg {
+  opacity: 0;
+  -webkit-backface-visibility: hidden;
+  backface-visibility: hidden;
+  /* ideally, transition speed should match zoom duration */
+  transition: all 0.3s ease-out;
+}
+
+.mfp-with-zoom.mfp-ready .mfp-container {
+  opacity: 1;
+}
+
+.mfp-with-zoom.mfp-ready.mfp-bg {
+  opacity: 0.8;
+}
+
+.mfp-with-zoom.mfp-removing .mfp-container,
+.mfp-with-zoom.mfp-removing.mfp-bg {
+  opacity: 0;
+}
+
+#fh5co-container {
+  background: #ffffff;
+}
+
+#fh5co-offcanvas,
+#fh5co-container,
+.fh5co-nav-toggle,
+#fh5co-footer {
+  transition: 0.5s;
+}
+
+#fh5co-container,
+.fh5co-nav-toggle {
+  z-index: 2;
+  position: relative;
+}
+
+#fh5co-offcanvas {
+  display: block;
+  height: 100%;
+  left: 0;
+  overflow-y: auto;
+  position: fixed;
+  z-index: 200;
+  top: 0;
+  bottom: 0;
+  width: 275px;
+  background: rgba(0, 0, 0, 0.9);
+  padding: 0.75em 1.25em;
+  transform: translateX(-275px);
+  transition: 0.9s;
+}
+#fh5co-offcanvas a {
+  display: block;
+  color: rgba(255, 255, 255, 0.4);
+  text-align: center;
+  padding: 7px 0;
+}
+#fh5co-offcanvas a:hover,
+#fh5co-offcanvas a:focus,
+#fh5co-offcanvas a:active {
+  outline: none;
+  text-decoration: none;
+  color: #ee1c24;
+  color: var(--theme-accent-color, #ee1c24);
+}
+#fh5co-offcanvas a.active {
+  color: #ee1c24;
+  color: var(--theme-accent-color, #ee1c24);
+}
+@media screen and (max-width: 768px) {
+  #fh5co-offcanvas {
+    display: block;
+  }
+}
+.offcanvas-visible #fh5co-offcanvas {
+  transform: translateX(0px);
+  transition: 0.5s;
+}
+
+@media screen and (max-width: 768px) {
+  #fh5co-container,
+  #fh5co-footer,
+  .fh5co-nav-toggle {
+    transform: translateX(0px);
+  }
+}
+.offcanvas-visible #fh5co-container,
+.offcanvas-visible #fh5co-footer,
+.offcanvas-visible .fh5co-nav-toggle {
+  transform: translateX(275px);
+}
+
+.js-sticky {
+  display: block;
+}
+@media screen and (max-width: 768px) {
+  .js-sticky {
+    display: none;
+  }
+}
+
+.fh5co-nav-toggle {
+  cursor: pointer;
+  text-decoration: none;
+}
+.fh5co-nav-toggle.active i::before,
+.fh5co-nav-toggle.active i::after {
+  background: #ffffff;
+}
+.fh5co-nav-toggle:hover,
+.fh5co-nav-toggle:focus,
+.fh5co-nav-toggle:active {
+  outline: none;
+  border-bottom: none !important;
+}
+.fh5co-nav-toggle i {
+  position: relative;
+  display: inline-block;
+  width: 30px;
+  height: 2px;
+  color: #ffffff;
+  font: bold 14px/.4 Helvetica;
+  font-family: Helvetica, "DejaVu Sans", "Open Sans", "Liberation Sans", Arial, sans-serif;
+  text-transform: uppercase;
+  text-indent: -55px;
+  background: #ffffff;
+  transition: all .2s ease-out;
+}
+.fh5co-nav-toggle i::before,
+.fh5co-nav-toggle i::after {
+  content: '';
+  width: 30px;
+  height: 2px;
+  background: #ffffff;
+  position: absolute;
+  left: 0;
+  transition: 0.2s;
+}
+
+.fh5co-nav-toggle i::before {
+  top: -7px;
+}
+
+.fh5co-nav-toggle i::after {
+  bottom: -7px;
+}
+
+.fh5co-nav-toggle:hover i::before {
+  top: -10px;
+}
+
+.fh5co-nav-toggle:hover i::after {
+  bottom: -10px;
+}
+
+.fh5co-nav-toggle.active i {
+  background: transparent;
+}
+
+.fh5co-nav-toggle.active i::before {
+  top: 0;
+  transform: rotateZ(45deg);
+}
+
+.fh5co-nav-toggle.active i::after {
+  bottom: 0;
+  transform: rotateZ(-45deg);
+}
+
+.fh5co-nav-toggle {
+  position: fixed;
+  top: 20px;
+  left: 20px;
+  z-index: 9999;
+  display: block;
+  margin: 0 auto;
+  display: none;
+  border-bottom: none !important;
+  background: rgba(0, 0, 0, 0.7);
+  padding: 0px 10px 10px 10px;
+  cursor: pointer;
+  border-radius: 4px;
+}
+@media screen and (max-width: 768px) {
+  .fh5co-nav-toggle {
+    display: block;
+  }
+}
+
+.header-row {
+  background: #e8ecf1;
+  margin-right: auto;
+  margin-left: auto;
+  margin-top: 1em;
+}
+
+.row-padded {
+  padding-bottom: 40px;
+}
+
+.no-js #loader {
+  display: none;
+}
+
+.js #loader {
+  display: block;
+  position: absolute;
+  left: 100px;
+  top: 0;
+  border: 10px solid #ee1c24;
+  border: 10px solid var(--theme-accent-color, #ee1c24);
+}
+
+.fh5co-loader {
+  position: fixed;
+  left: 0px;
+  top: 0px;
+  width: 100%;
+  height: 100%;
+  z-index: 9999;
+  background: url(../images/Preloader_2.gif) center no-repeat #ffffff;
+}
+
+
+/*
+* Additions Specific to Lektor Version
+*/
+
+/* General format tweaks */
+
+mark {
+  background-color: #ffff00;
+  color: #000000;
+}
+
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+sub {
+  bottom: -.25em;
+}
+
+sup {
+  top: -.5em;
+}
+
+em,
+i,
+dfn {
+  font-style: italic;
+}
+
+b,
+em,
+i,
+strong {
+  line-height: inherit;
+}
+
+b,
+strong {
+  font-weight: 700;
+  font-weight: bolder;
+}
+
+small,
+.small {
+  font-size: 85%;
+}
+
+small {
+  line-height: inherit;
+  color: #ffffff;
+}
+
+h1 small,
+h2 small,
+h3 small,
+h4 small,
+h5 small,
+h6 small {
+  line-height: 0;
+  color: #cccccc;
+}
+
+h1 {
+    font-size: 3rem;
+}
+h2 {
+    font-size: 2.5rem;
+}
+h3 {
+    font-size: 1.9375rem;
+}
+h4 {
+    font-size: 1.5625rem;
+}
+h5 {
+    font-size: 1.25rem;
+}
+h6 {
+    font-size: 1rem;
+}
+
+a img {
+  border: 0;
+}
+
+small a,
+small a:focus {
+  color: #ffffff;
+  font-weight: bold;
+}
+
+li a {
+  color: #ee1c24;
+  color: var(--theme-accent-color, #ee1c24);
+}
+
+dl,
+ol,
+ul {
+  list-style-position: outside;
+}
+
+li {
+  font-size: inherit;
+}
+
+ul {
+  list-style-type: disc;
+}
+
+ol,
+ul {
+  margin-left: 1.25rem;
+  margin-top: 0;
+  margin-bottom: 10px;
+}
+
+ol ol,
+ol ul,
+ul ol,
+ul ul {
+  margin-left: 1.25rem;
+  margin-bottom: 0;
+}
+
+
+/* Columns and layout */
+
+.text-center {
+  text-align: center;
+}
+
+.column,
+.columns {
+  width: 100%;
+  float: left;
+}
+
+.column:last-child:not(:first-child),
+.columns:last-child:not(:first-child) {
+  float: right;
+}
+
+.row {
+  max-width: 100rem;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.row:before,
+.row:after {
+  content: " ";
+  display: table;
+}
+
+.row:after {
+  clear: both;
+}
+
+.col-md-4,
+.col-md-6 {
+  float: left;
+  min-height: 1px;
+  padding-left: 15px;
+  padding-right: 15px;
+  position: relative;
+}
+
+.col-md-4 {
+    width: 33.33%;
+}
+
+.col-md-6 {
+    width: 50%;
+}
+
+@media screen and (max-width: 1200px) {
+  .col-md-4, .col-md-6 {
+    width: 50%;
+  }
+}
+@media screen and (max-width: 768px) {
+  .col-md-4, .col-md-6 {
+    float: none;
+    width: auto;
+  }
+}
+
+.small-1 {
+  width: 8.33333%;
+}
+
+.small-6 {
+  width: 50%;
+}
+
+@media screen and (max-width: 1200px) {
+  .content-section .center,
+  .mission-section .center,
+  #blog-center {
+    width: 65%;
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .content-section .center,
+  .mission-section .center,
+  #blog-center {
+    width: 80%;
+  }
+}
+
+.service .icon .img-responsive {
+  margin-bottom: 0.5em;
+  width: 80%;
+}
+
+
+
+/* Verbatim styling */
+
+figure {
+  margin: 0;
+}
+
+code,
+kbd,
+pre,
+samp {
+  font-family: Inconsolata, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
+  font-size: 0.9em;
+  color: #333333;
+}
+
+code {
+  background-color: #f9f2f4;
+  border-radius: 4px;
+  font-weight: 400;
+  padding: .125rem .3125rem .0625rem;
+}
+
+p code {
+  background-color: #f5f5f5;
+  border: 1px solid #cccccc;
+}
+
+kbd {
+  background-color: #333333;
+  border-radius: 3px;
+  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);
+  margin: 0;
+  padding: .125rem .25rem 0;
+}
+
+kbd kbd {
+  font-size: 100%;
+  padding: 0;
+  font-weight: bold;
+  box-shadow: none;
+}
+
+pre {
+  display: block;
+  padding: 9.5px;
+  margin: 0 0 10px;
+  margin-bottom: 22px;
+  line-height: 1.42857;
+  word-break: break-all;
+  word-wrap: break-word;
+  background-color: #f5f5f5;
+  border: 1px solid #cccccc;
+  border-radius: 4px;
+  overflow: auto;
+}
+
+pre code {
+  padding: 0;
+  font-size: inherit;
+  color: inherit;
+  white-space: pre-wrap;
+  background-color: transparent;
+  border-radius: 0;
+}
+
+.pre-scrollable {
+  max-height: 340px;
+  overflow-y: scroll;
+}
+
+
+/* Button styling from Bootstrap */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  color: inherit;
+  font-size: 100%;
+  line-height: 1.15;
+  margin: 0;
+}
+
+button {
+  overflow: visible;
+  padding: 0;
+  -webkit-appearance: none;
+  -moz-appearance: none;
+  appearance: none;
+  border: 0;
+  border-radius: 0;
+  background: transparent;
+  line-height: 1;
+}
+
+[data-whatinput=mouse] button {
+  outline: 0;
+}
+
+button,
+select {
+  text-transform: none;
+}
+
+[type=reset],
+[type=submit],
+button,
+html [type=button] {
+  -webkit-appearance: button;
+  -moz-appearance: button;
+  cursor: pointer;
+}
+
+[type=button],
+[type=submit] {
+-webkit-appearance: none;
+-moz-appearance: none;
+appearance: none;
+border-radius: 0;
+}
+
+
+/* Navbar */
+
+.menu {
+  list-style-type: none;
+}
+
+.menu > li {
+  display: table-cell;
+  vertical-align: middle;
+}
+
+[data-whatinput=mouse] .menu > li {
+  outline: 0;
+}
+
+.menu > li > a {
+  display: block;
+  padding: .7rem 1rem;
+  line-height: 1;
+}
+
+.menu a,
+.menu button,
+.menu input,
+.menu select {
+  margin-bottom: 0;
+}
+
+.menu > li > a i,
+.menu > li > a i + span,
+.menu > li > a img,
+.menu > li > a img + span,
+.menu > li > a svg,
+.menu > li > a svg + span {
+  vertical-align: middle;
+}
+
+.menu > li > a i,
+.menu > li > a img,
+.menu > li > a svg {
+  margin-right: .25rem;
+  display: inline-block;
+}
+
+.menu.horizontal > li,
+.menu > li {
+  display: table-cell;
+}
+
+.navbar {
+  position: relative;
+  min-height: 55px;
+  margin-bottom: 20px;
+  border-color: transparent;
+  border-radius: 4px;
+  border-style: solid;
+  border-width: 1px;
+}
+
+.navbar-header {
+  float: left;
+}
+
+.navbar:before,
+.navbar:after,
+.navbar-header:before,
+.navbar-header:after {
+  content: " ";
+  display: table;
+}
+
+.navbar:after,
+.navbar-header:after {
+  clear: both;
+}
+
+@media screen and (max-width: 768px) {
+  .navbar {
+    border-radius: 0;
+  }
+  .navbar-header {
+    float: none;
+  }
+}
+
+
+/* Navbar logo */
+
+#menu-logo a {
+  padding-bottom: 0;
+  padding-top: 0;
+}
+
+#menu-logo .logo-text-container {
+  display: inline-block;
+  color: #333333;
+  vertical-align: middle;
+}
+
+#menu-logo .logo-text {
+  display: block;
+  font-size: 2.2em;
+  font-family: Candara, Raleway, "DejaVu Sans", "Open Sans", "Liberation Sans", Helvetica, Arial, sans-serif;
+  margin-top: 2px;
+}
+
+#menu-logo .logo-text .small-caps {
+  font-size: 80%;
+}
+
+#menu-logo img {
+  width: 3.7em;
+  vertical-align: middle;
+}
+
+#menu-logo > p {
+  margin-bottom: 1rem;
+}
+
+.fh5co-main-nav .fh5co-menu-1 .dropdown {
+  list-style-type: none;
+  padding: 0;
+  margin: 0;
+}
+
+
+/* Blog */
+
+.blog-post {
+  margin-top: 2rem;
+  margin-bottom: 2rem;
+  font-family: Raleway, "DejaVu Sans", "Open Sans", "Liberation Sans", Helvetica, Arial, sans-serif;
+  color: #505050;
+}
+
+.blog-post img,
+.page-content .center img {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+  margin-bottom: 20px;
+  vertical-align: middle;
+  max-width: 100%;
+  height: auto;
+}
+
+.blog-post p,
+.blog-post ul,
+.blog-post ol {
+  margin-bottom: 1.4rem;
+  font-size: inherit;
+  line-height: 1.57;
+  text-rendering: optimizeLegibility;
+}
+
+.blog-post p img {
+  margin-top: 20px;
+}
+
+.blog-post a {
+  line-height: inherit;
+  color: #ee1c24;
+  color: var(--theme-accent-color, #ee1c24);
+  cursor: pointer;
+  text-decoration: none;
+  border-bottom: 1px dotted #ee1c24;
+  border-bottom: 1px dotted var(--theme-accent-color, #ee1c24);
+}
+
+.blog-post h1 a,
+.blog-post h2 a,
+.blog-post h3 a {
+  text-decoration: none;
+  color: #000000;
+  border: 0;
+}
+
+.blog-post h1,
+.blog-post h2,
+.blog-post h3 {
+  text-align: left;
+  color: #000000;
+  font-family: Amiri, "Open Serif", "Liberation Serif", "DejaVu Serif", Times, "Times New Roman", serif;
+}
+
+.blog-post h1 {
+  margin-bottom: 25px;
+  font-size: 2.4em;
+}
+
+.blog-post h2 {
+  margin-bottom: 15px;
+  font-size: 1.8em;
+}
+
+.blog-post h3 {
+  margin-bottom: 10px;
+  font-size: 1.5em;
+}
+
+.blog-post .blog-index-header .row h1,
+.blog-post .blog-index-header .row h2,
+.blog-post .blog-index-header .row h3 {
+  text-align: left;
+  color: #000000;
+  font-family: Amiri, "Open Serif", "Liberation Serif", "DejaVu Serif", Times, "Times New Roman", serif;
+}
+
+.blog-post .blog-index-header .row h1 {
+  margin-bottom: 20px;
+}
+
+.blog-post .blog-index-header .row h2 {
+  font-size: 1.5em;
+  margin-bottom: 15px;
+}
+
+.blog-post .blog-index-header .row h3 {
+  margin-bottom: 10px;
+}
+
+.author-img {
+  width: 3.5rem;
+  border-radius: 2.5rem;
+  border: .35rem;
+  padding: .23rem;
+}
+
+.meta-blog-author,
+.meta-blog-date,
+.meta-blog-index {
+  color: #000000;
+  font-size: .9em;
+}
+
+.blog-index-header {
+  width: 90%;
+  margin: 0;
+}
+
+.meta-blog-index .author {
+  text-align: center;
+}
+
+.blog-post-header {
+  padding-top: 1.5rem;
+}
+
+
+.meta-blog-date {
+  text-align: right;
+}
+
+.meta-blog-author {
+  margin-right: 0.2em;
+  height: 56px;
+  width: 56px;
+}
+
+.author-img,
+.meta-blog-author img {
+  display: block;
+  max-height: 56px;
+  max-width: 56px;
+  min-height: 56px;
+  min-width: 56px;
+}
+
+.blog-pagination {
+  clear: both;
+  margin-right: auto;
+  margin-left: auto;
+}
+
+.blog-pagination .next,
+.blog-pagination .previous {
+  text-align: center;
+}
+
+.blog-content-container {
+  margin-bottom: 5em;
+  margin-top: 0;
+}
+
+#blog-main-title {
+  margin-top: 3em;
+  margin-bottom: 0;
+}
+
+#blog-main-title h1 {
+  font-family: Amiri, "Open Serif", "Liberation Serif", "DejaVu Serif", Times, "Times New Roman", serif;
+  font-size: 2em;
+  margin-bottom: 0;
+}
+
+
+/* Blog buttons */
+
+.button {
+  display: inline-block;
+  vertical-align: middle;
+  margin: 0 0 1rem;
+  padding: .85em 1em;
+  -webkit-appearance: none;
+  -moz-appearance: none;
+  appearance: none;
+  border: 1px solid transparent;
+  border-radius: 0;
+  transition: background-color .25s ease-out, color .25s ease-out;
+  font-size: .9em;
+  line-height: 1;
+  text-align: center;
+  cursor: pointer;
+  background-color: #ee1c24;
+  background-color: var(--theme-accent-color, #ee1c24);
+  color: #ffffff;
+}
+
+[data-whatinput=mouse] .button {
+  outline: 0;
+}
+
+.button:focus,
+.button:hover {
+  background-color: #450200;
+  color: #ffffff;
+}
+
+.button.disabled,
+.button[disabled] {
+  opacity: .25;
+  cursor: not-allowed;
+}
+
+.button.disabled:focus,
+.button.disabled:hover,
+.button[disabled]:focus,
+.button[disabled]:hover {
+  background-color: #450200;
+  color: #ffffff;
+}
+
+
+/* Footer */
+
+#fh5co-footer {
+  padding-top: 0.9em;
+  padding-bottom: 0.6em;
+  color: #ffffff;
+}
+
+#fh5co-footer .container .row .text-center {
+  margin-bottom: 0.7em;
+}
+
+#fh5co-footer .container .footer-connect-line .text-center p {
+  font-size: 16px;
+  line-height: 2em;
+}
+
+#fh5co-footer .container .footer-copyright .text-center p {
+  font-size: 13px;
+  line-height: 1.2em;
+}
+
+#fh5co-footer a,
+#fh5co-footer a:focus {
+  color: #ffffff;
+  font-weight: bold;
+}
+
+
+/* Specific feature additions */
+
+.center,
+#blog-main-title {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+  width: 51%;
+}
+
+.content-section .content-button {
+  margin-top: 0.5em;
+}
+
+.content-section .image-button {
+  min-width: 480px;
+  width: 480px;
+}
+
+@media screen and (max-width: 768px) {
+  .content-section .content-button {
+    margin-top: 1em;
+    width: 100%;
+  }
+}
+
+.content-section .image-button img.center {
+  width: 100%;
+}
+
+.content-section .text-button {
+  background-color: #ee1c24;
+  background-color: var(--theme-accent-color, #ee1c24);
+  border: none;
+  border-radius: 1.4em;
+  color: white;
+  cursor: pointer;
+  display: table;
+  font-size: 1.4em;
+  font-weight: bold;
+  margin-left: auto;
+  margin-right: auto;
+  padding-top: 0.5em;
+  padding-bottom: 0.5em;
+  padding-left: 2em;
+  padding-right: 2em;
+  text-align: center;
+  text-decoration: none;
+  min-width: 20%;
+  max-width: 80%;
+}
+
+#open_chat {
+  background-color: #ee1c24;
+  background-color: var(--theme-accent-color, #ee1c24);
+  font-weight: bold;
+}
+
+.pipe-colored {
+  color: #ff4c52;
+  color: var(--theme-pipe-color, #ff4c52);
+  font-weight: bold;
+}
+
+#blog-center .comment-box {
+  margin-top: 4rem;
+}
+
+#rss-nav-link {
+  background-color: #ff4c52;
+  background-color: var(--theme-pipe-color, #ff4c52);
+  border: none;
+  border-radius: 0.5em;
+  color: #ffffff;
+  padding-left: 0.5em;
+  padding-right: 0.35em;
+}
+
+.card-inner {
+  overflow: hidden;
+}
+
+.mission-section.notitle-section,
+.gallery-section.notitle-section {
+  margin: 0;
+  padding: 0;
+}
+
+.mission-section.notitle-section .fh5co-tabs-container {
+  padding-top: 2em;
+}
+
+
+/* Generic page styling */
+
+.page-content-container {
+  background-color: #ffffff;
+  overflow: auto;
+}
+
+.page-content {
+  margin-top: 3em;
+  margin-bottom: 4em;
+  margin-left: auto;
+  margin-right: auto;
+  max-width: 1100px;
+  width: 75%;
+}
+
+.page-content .text-center {
+  margin-bottom: 2em;
+}
+
+.page-content .center {
+  width: 100%;
+}
+
+.page-content .rule-under-heading {
+  position: inherit;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+
+/* 404 Error page styling */
+
+#error-page {
+  background-color: #e8ecf1;
+  background-position: center center;
+  background-repeat: no-repeat;
+  background-size: cover;
+  min-height: 480px;
+  overflow: hidden;
+  padding-top: 4em;
+}
+
+#error-page .page-content {
+  background-color: rgba(232, 236, 241, 0.5);
+  border-radius: 50px;
+  margin-top: 5em;
+  margin-bottom: 6em;
+  padding-top: 2em;
+  padding-bottom: 2em;
+}
+
+#error-page .fh5co-heading {
+  margin-bottom: 3em;
+}
+
+#error-page .center {
+  padding-left: 3em;
+  padding-right: 3em;
+}
+
+#error-page .center p {
+  text-align: center;
+  font-size: 1.4em;
+}
+
+#error-page .center img {
+  margin-top: 1em;
+  margin-bottom: 1em;
+}
diff --git a/website/themes/lektor-icon/assets/static/fonts/bootstrap/glyphicons-halflings-regular.ttf b/website/themes/lektor-icon/assets/static/fonts/bootstrap/glyphicons-halflings-regular.ttf
new file mode 100644
index 0000000..1413fc6
Binary files /dev/null and b/website/themes/lektor-icon/assets/static/fonts/bootstrap/glyphicons-halflings-regular.ttf differ
diff --git a/website/themes/lektor-icon/assets/static/fonts/bootstrap/glyphicons-halflings-regular.woff b/website/themes/lektor-icon/assets/static/fonts/bootstrap/glyphicons-halflings-regular.woff
new file mode 100644
index 0000000..9e61285
Binary files /dev/null and b/website/themes/lektor-icon/assets/static/fonts/bootstrap/glyphicons-halflings-regular.woff differ
diff --git a/website/themes/lektor-icon/assets/static/fonts/bootstrap/glyphicons-halflings-regular.woff2 b/website/themes/lektor-icon/assets/static/fonts/bootstrap/glyphicons-halflings-regular.woff2
new file mode 100644
index 0000000..64539b5
Binary files /dev/null and b/website/themes/lektor-icon/assets/static/fonts/bootstrap/glyphicons-halflings-regular.woff2 differ
diff --git a/website/themes/lektor-icon/assets/static/fonts/icomoon/icomoon.ttf b/website/themes/lektor-icon/assets/static/fonts/icomoon/icomoon.ttf
new file mode 100644
index 0000000..5e11293
Binary files /dev/null and b/website/themes/lektor-icon/assets/static/fonts/icomoon/icomoon.ttf differ
diff --git a/website/themes/lektor-icon/assets/static/fonts/icomoon/icomoon.woff b/website/themes/lektor-icon/assets/static/fonts/icomoon/icomoon.woff
new file mode 100644
index 0000000..afcece6
Binary files /dev/null and b/website/themes/lektor-icon/assets/static/fonts/icomoon/icomoon.woff differ
diff --git a/website/themes/lektor-icon/assets/static/images/Preloader_2.gif b/website/themes/lektor-icon/assets/static/images/Preloader_2.gif
new file mode 100644
index 0000000..1455a25
Binary files /dev/null and b/website/themes/lektor-icon/assets/static/images/Preloader_2.gif differ
diff --git a/website/themes/lektor-icon/assets/static/images/placeholder_person.png b/website/themes/lektor-icon/assets/static/images/placeholder_person.png
new file mode 100644
index 0000000..be2c992
Binary files /dev/null and b/website/themes/lektor-icon/assets/static/images/placeholder_person.png differ
diff --git a/website/themes/lektor-icon/assets/static/js/jquery-3.3.1.min.js b/website/themes/lektor-icon/assets/static/js/jquery-3.3.1.min.js
new file mode 100644
index 0000000..4d9b3a2
--- /dev/null
+++ b/website/themes/lektor-icon/assets/static/js/jquery-3.3.1.min.js
@@ -0,0 +1,2 @@
+/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */
+!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:s,sort:n.sort,splice:n.splice},w.extend=w.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||g(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)n=a[t],a!==(r=e[t])&&(l&&r&&(w.isPlainObject(r)||(i=Array.isArray(r)))?(i?(i=!1,o=n&&Array.isArray(n)?n:[]):o=n&&w.isPlainObject(n)?n:{},a[t]=w.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},w.extend({expando:"jQuery"+("3.3.1"+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==c.call(e))&&(!(t=i(e))||"function"==typeof(n=f.call(t,"constructor")&&t.constructor)&&p.call(n)===d)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e){m(e)},each:function(e,t){var n,r=0;if(C(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},trim:function(e){return null==e?"":(e+"").replace(T,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(C(Object(e))?w.merge(n,"string"==typeof e?[e]:e):s.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:u.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r,i=[],o=0,a=e.length,s=!n;o<a;o++)(r=!t(e[o],o))!==s&&i.push(e[o]);return i},map:function(e,t,n){var r,i,o=0,s=[];if(C(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&s.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&s.push(i);return a.apply([],s)},guid:1,support:h}),"function"==typeof Symbol&&(w.fn[Symbol.iterator]=n[Symbol.iterator]),w.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){l["[object "+t+"]"]=t.toLowerCase()});function C(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!g(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},P="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\0-\\xa0])+",I="\\["+M+"*("+R+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+R+"))|)"+M+"*\\]",W=":("+R+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+I+")*)|.*)\\)|)",$=new RegExp(M+"+","g"),B=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),F=new RegExp("^"+M+"*,"+M+"*"),_=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="<a id='"+b+"'></a><select id='"+b+"-\r\\' msallowcapture=''><option selected=''></option></select>",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:he(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:he(function(e,t,n){for(var r=n<0?n+t:n;--r>=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}}).pseudos.nth=r.pseudos.eq;for(t in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})r.pseudos[t]=fe(t);for(t in{submit:!0,reset:!0})r.pseudos[t]=pe(t);function ye(){}ye.prototype=r.filters=r.pseudos,r.setFilters=new ye,a=oe.tokenize=function(e,t){var n,i,o,a,s,u,l,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,u=[],l=r.preFilter;while(s){n&&!(i=F.exec(s))||(i&&(s=s.slice(i[0].length)||s),u.push(o=[])),n=!1,(i=_.exec(s))&&(n=i.shift(),o.push({value:n,type:i[0].replace(B," ")}),s=s.slice(n.length));for(a in r.filter)!(i=V[a].exec(s))||l[a]&&!(i=l[a](i))||(n=i.shift(),o.push({value:n,type:a,matches:i}),s=s.slice(n.length));if(!n)break}return t?s.length:s?oe.error(e):k(e,u).slice(0)};function ve(e){for(var t=0,n=e.length,r="";t<n;t++)r+=e[t].value;return r}function me(e,t,n){var r=t.dir,i=t.next,o=i||r,a=n&&"parentNode"===o,s=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||a)return e(t,n,i);return!1}:function(t,n,u){var l,c,f,p=[T,s];if(u){while(t=t[r])if((1===t.nodeType||a)&&e(t,n,u))return!0}else while(t=t[r])if(1===t.nodeType||a)if(f=t[b]||(t[b]={}),c=f[t.uniqueID]||(f[t.uniqueID]={}),i&&i===t.nodeName.toLowerCase())t=t[r]||t;else{if((l=c[o])&&l[0]===T&&l[1]===s)return p[2]=l[2];if(c[o]=p,p[2]=e(t,n,u))return!0}return!1}}function xe(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r<i;r++)oe(e,t[r],n);return n}function we(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s<u;s++)(o=e[s])&&(n&&!n(o,r,i)||(a.push(o),l&&t.push(s)));return a}function Te(e,t,n,r,i,o){return r&&!r[b]&&(r=Te(r)),i&&!i[b]&&(i=Te(i,o)),se(function(o,a,s,u){var l,c,f,p=[],d=[],h=a.length,g=o||be(t||"*",s.nodeType?[s]:s,[]),y=!e||!o&&t?g:we(g,p,e,s,u),v=n?i||(o?e:h||r)?[]:a:y;if(n&&n(y,v,s,u),r){l=we(v,d),r(l,[],s,u),c=l.length;while(c--)(f=l[c])&&(v[d[c]]=!(y[d[c]]=f))}if(o){if(i||e){if(i){l=[],c=v.length;while(c--)(f=v[c])&&l.push(y[c]=f);i(null,v=[],l,u)}c=v.length;while(c--)(f=v[c])&&(l=i?O(o,f):p[c])>-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u<o;u++)if(n=r.relative[e[u].type])p=[me(xe(p),n)];else{if((n=r.filter[e[u].type].apply(null,e[u].matches))[b]){for(i=++u;i<o;i++)if(r.relative[e[i].type])break;return Te(u>1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u<i&&Ce(e.slice(u,i)),i<o&&Ce(e=e.slice(i)),i<o&&ve(e))}p.push(n)}return xe(p)}function Ee(e,t){var n=t.length>0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t<r;t++)if(w.contains(i[t],this))return!0}));for(n=this.pushStack([]),t=0;t<r;t++)w.find(e,i[t],n);return r>1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e<n;e++)if(w.contains(this,t[e]))return!0})},closest:function(e,t){var n,r=0,i=this.length,o=[],a="string"!=typeof e&&w(e);if(!D.test(e))for(;r<i;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?a.index(n)>-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s<o.length)!1===o[s].apply(n[0],n[1])&&e.stopOnFalse&&(s=o.length,n=!1)}e.memory||(n=!1),t=!1,i&&(o=n?[]:"")},l={add:function(){return o&&(n&&!t&&(s=o.length-1,a.push(n)),function t(n){w.each(n,function(n,r){g(r)?e.unique&&l.has(r)||o.push(r):r&&r.length&&"string"!==x(r)&&t(r)})}(arguments),n&&!t&&u()),this},remove:function(){return w.each(arguments,function(e,t){var n;while((n=w.inArray(t,o,n))>-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t<o)){if((e=r.apply(s,u))===n.promise())throw new TypeError("Thenable self-resolution");l=e&&("object"==typeof e||"function"==typeof e)&&e.then,g(l)?i?l.call(e,a(o,n,I,i),a(o,n,W,i)):(o++,l.call(e,a(o,n,I,i),a(o,n,W,i),a(o,n,I,n.notifyWith))):(r!==I&&(s=void 0,u=[e]),(i||n.resolveWith)(s,u))}},c=i?l:function(){try{l()}catch(e){w.Deferred.exceptionHook&&w.Deferred.exceptionHook(e,c.stackTrace),t+1>=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s<u;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:l?t.call(e):u?t(e[0],n):o},X=/^-ms-/,U=/-([a-z])/g;function V(e,t){return t.toUpperCase()}function G(e){return e.replace(X,"ms-").replace(U,V)}var Y=function(e){return 1===e.nodeType||9===e.nodeType||!+e.nodeType};function Q(){this.expando=w.expando+Q.uid++}Q.uid=1,Q.prototype={cache:function(e){var t=e[this.expando];return t||(t={},Y(e)&&(e.nodeType?e[this.expando]=t:Object.defineProperty(e,this.expando,{value:t,configurable:!0}))),t},set:function(e,t,n){var r,i=this.cache(e);if("string"==typeof t)i[G(t)]=n;else for(r in t)i[G(r)]=t[r];return i},get:function(e,t){return void 0===t?this.cache(e):e[this.expando]&&e[this.expando][G(t)]},access:function(e,t,n){return void 0===t||t&&"string"==typeof t&&void 0===n?this.get(e,t):(this.set(e,t,n),void 0!==n?n:t)},remove:function(e,t){var n,r=e[this.expando];if(void 0!==r){if(void 0!==t){n=(t=Array.isArray(t)?t.map(G):(t=G(t))in r?[t]:t.match(M)||[]).length;while(n--)delete r[t[n]]}(void 0===t||w.isEmptyObject(r))&&(e.nodeType?e[this.expando]=void 0:delete e[this.expando])}},hasData:function(e){var t=e[this.expando];return void 0!==t&&!w.isEmptyObject(t)}};var J=new Q,K=new Q,Z=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,ee=/[A-Z]/g;function te(e){return"true"===e||"false"!==e&&("null"===e?null:e===+e+""?+e:Z.test(e)?JSON.parse(e):e)}function ne(e,t,n){var r;if(void 0===n&&1===e.nodeType)if(r="data-"+t.replace(ee,"-$&").toLowerCase(),"string"==typeof(n=e.getAttribute(r))){try{n=te(n)}catch(e){}K.set(e,t,n)}else n=void 0;return n}w.extend({hasData:function(e){return K.hasData(e)||J.hasData(e)},data:function(e,t,n){return K.access(e,t,n)},removeData:function(e,t){K.remove(e,t)},_data:function(e,t,n){return J.access(e,t,n)},_removeData:function(e,t){J.remove(e,t)}}),w.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if(void 0===e){if(this.length&&(i=K.get(o),1===o.nodeType&&!J.get(o,"hasDataAttrs"))){n=a.length;while(n--)a[n]&&0===(r=a[n].name).indexOf("data-")&&(r=G(r.slice(5)),ne(o,r,i[r]));J.set(o,"hasDataAttrs",!0)}return i}return"object"==typeof e?this.each(function(){K.set(this,e)}):z(this,function(t){var n;if(o&&void 0===t){if(void 0!==(n=K.get(o,e)))return n;if(void 0!==(n=ne(o,e)))return n}else this.each(function(){K.set(this,e,t)})},null,t,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length<n?w.queue(this[0],e):void 0===t?this:this.each(function(){var n=w.queue(this,e,t);w._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&w.dequeue(this,e)})},dequeue:function(e){return this.each(function(){w.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=w.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=void 0),e=e||"fx";while(a--)(n=J.get(o[a],e+"queueHooks"))&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var re=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,ie=new RegExp("^(?:([+-])=|)("+re+")([a-z%]*)$","i"),oe=["Top","Right","Bottom","Left"],ae=function(e,t){return"none"===(e=t||e).style.display||""===e.style.display&&w.contains(e.ownerDocument,e)&&"none"===w.css(e,"display")},se=function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i};function ue(e,t,n,r){var i,o,a=20,s=r?function(){return r.cur()}:function(){return w.css(e,t,"")},u=s(),l=n&&n[3]||(w.cssNumber[t]?"":"px"),c=(w.cssNumber[t]||"px"!==l&&+u)&&ie.exec(w.css(e,t));if(c&&c[3]!==l){u/=2,l=l||c[3],c=+u||1;while(a--)w.style(e,t,c+l),(1-o)*(1-(o=s()/u||.5))<=0&&(a=0),c/=o;c*=2,w.style(e,t,c+l),n=n||[]}return n&&(c=+c||+u||0,i=n[1]?c+(n[1]+1)*n[2]:+n[2],r&&(r.unit=l,r.start=c,r.end=i)),i}var le={};function ce(e){var t,n=e.ownerDocument,r=e.nodeName,i=le[r];return i||(t=n.body.appendChild(n.createElement(r)),i=w.css(t,"display"),t.parentNode.removeChild(t),"none"===i&&(i="block"),le[r]=i,i)}function fe(e,t){for(var n,r,i=[],o=0,a=e.length;o<a;o++)(r=e[o]).style&&(n=r.style.display,t?("none"===n&&(i[o]=J.get(r,"display")||null,i[o]||(r.style.display="")),""===r.style.display&&ae(r)&&(i[o]=ce(r))):"none"!==n&&(i[o]="none",J.set(r,"display",n)));for(o=0;o<a;o++)null!=i[o]&&(e[o].style.display=i[o]);return e}w.fn.extend({show:function(){return fe(this,!0)},hide:function(){return fe(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){ae(this)?w(this).show():w(this).hide()})}});var pe=/^(?:checkbox|radio)$/i,de=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n<r;n++)J.set(e[n],"globalEval",!t||J.get(t[n],"globalEval"))}var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d<h;d++)if((o=e[d])||0===o)if("object"===x(o))w.merge(p,o.nodeType?[o]:o);else if(me.test(o)){a=a||f.appendChild(t.createElement("div")),s=(de.exec(o)||["",""])[1].toLowerCase(),u=ge[s]||ge._default,a.innerHTML=u[1]+w.htmlPrefilter(o)+u[2],c=u[0];while(c--)a=a.lastChild;w.merge(p,a.childNodes),(a=f.firstChild).textContent=""}else p.push(t.createTextNode(o));f.textContent="",d=0;while(o=p[d++])if(r&&w.inArray(o,r)>-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="<textarea>x</textarea>",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n<arguments.length;n++)u[n]=arguments[n];if(t.delegateTarget=this,!c.preDispatch||!1!==c.preDispatch.call(this,t)){s=w.event.handlers.call(this,t,l),n=0;while((o=s[n++])&&!t.isPropagationStopped()){t.currentTarget=o.elem,r=0;while((a=o.handlers[r++])&&!t.isImmediatePropagationStopped())t.rnamespace&&!t.rnamespace.test(a.namespace)||(t.handleObj=a,t.data=a.data,void 0!==(i=((w.event.special[a.origType]||{}).handle||a.handler).apply(o.elem,u))&&!1===(t.result=i)&&(t.preventDefault(),t.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,t),t.result}},handlers:function(e,t){var n,r,i,o,a,s=[],u=t.delegateCount,l=e.target;if(u&&l.nodeType&&!("click"===e.type&&e.button>=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n<u;n++)void 0===a[i=(r=t[n]).selector+" "]&&(a[i]=r.needsContext?w(i,this).index(l)>-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u<t.length&&s.push({elem:l,handlers:t.slice(u)}),s},addProp:function(e,t){Object.defineProperty(w.Event.prototype,e,{enumerable:!0,configurable:!0,get:g(t)?function(){if(this.originalEvent)return t(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[e]},set:function(t){Object.defineProperty(this,e,{enumerable:!0,configurable:!0,writable:!0,value:t})}})},fix:function(e){return e[w.expando]?e:new w.Event(e)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==Se()&&this.focus)return this.focus(),!1},delegateType:"focusin"},blur:{trigger:function(){if(this===Se()&&this.blur)return this.blur(),!1},delegateType:"focusout"},click:{trigger:function(){if("checkbox"===this.type&&this.click&&N(this,"input"))return this.click(),!1},_default:function(e){return N(e.target,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}}},w.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n)},w.Event=function(e,t){if(!(this instanceof w.Event))return new w.Event(e,t);e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&!1===e.returnValue?Ee:ke,this.target=e.target&&3===e.target.nodeType?e.target.parentNode:e.target,this.currentTarget=e.currentTarget,this.relatedTarget=e.relatedTarget):this.type=e,t&&w.extend(this,t),this.timeStamp=e&&e.timeStamp||Date.now(),this[w.expando]=!0},w.Event.prototype={constructor:w.Event,isDefaultPrevented:ke,isPropagationStopped:ke,isImmediatePropagationStopped:ke,isSimulated:!1,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=Ee,e&&!this.isSimulated&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=Ee,e&&!this.isSimulated&&e.stopPropagation()},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=Ee,e&&!this.isSimulated&&e.stopImmediatePropagation(),this.stopPropagation()}},w.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(e){var t=e.button;return null==e.which&&we.test(e.type)?null!=e.charCode?e.charCode:e.keyCode:!e.which&&void 0!==t&&Te.test(e.type)?1&t?1:2&t?3:4&t?2:0:e.which}},w.event.addProp),w.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,t){w.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return i&&(i===r||w.contains(r,i))||(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),w.fn.extend({on:function(e,t,n,r){return De(this,e,t,n,r)},one:function(e,t,n,r){return De(this,e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,w(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return!1!==t&&"function"!=typeof t||(n=t,t=void 0),!1===n&&(n=ke),this.each(function(){w.event.remove(this,e,n,t)})}});var Ne=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/<script|<style|<link/i,je=/checked\s*(?:[^=]|=\s*.checked.)/i,qe=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n<r;n++)w.event.add(t,i,l[i][n])}K.hasData(e)&&(s=K.access(e),u=w.extend({},s),K.set(t,u))}}function Me(e,t){var n=t.nodeName.toLowerCase();"input"===n&&pe.test(e.type)?t.checked=e.checked:"input"!==n&&"textarea"!==n||(t.defaultValue=e.defaultValue)}function Re(e,t,n,r){t=a.apply([],t);var i,o,s,u,l,c,f=0,p=e.length,d=p-1,y=t[0],v=g(y);if(v||p>1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f<p;f++)l=i,f!==d&&(l=w.clone(l,!0,!0),u&&w.merge(s,ye(l,"script"))),n.call(e[f],l,f);if(u)for(c=s[s.length-1].ownerDocument,w.map(s,Oe),f=0;f<u;f++)l=s[f],he.test(l.type||"")&&!J.access(l,"globalEval")&&w.contains(c,l)&&(l.src&&"module"!==(l.type||"").toLowerCase()?w._evalUrl&&w._evalUrl(l.src):m(l.textContent.replace(qe,""),c,l))}return e}function Ie(e,t,n){for(var r,i=t?w.filter(t,e):e,o=0;null!=(r=i[o]);o++)n||1!==r.nodeType||w.cleanData(ye(r)),r.parentNode&&(n&&w.contains(r.ownerDocument,r)&&ve(ye(r,"script")),r.parentNode.removeChild(r));return e}w.extend({htmlPrefilter:function(e){return e.replace(Ne,"<$1></$2>")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r<i;r++)Me(o[r],a[r]);if(t)if(n)for(o=o||ye(e),a=a||ye(s),r=0,i=o.length;r<i;r++)Pe(o[r],a[r]);else Pe(e,s);return(a=ye(s,"script")).length>0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n<r;n++)1===(t=this[n]||{}).nodeType&&(w.cleanData(ye(t,!1)),t.innerHTML=e);t=0}catch(e){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=[];return Re(this,arguments,function(t){var n=this.parentNode;w.inArray(this,e)<0&&(w.cleanData(ye(this)),n&&n.replaceChild(t,this))},e)}}),w.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){w.fn[e]=function(e){for(var n,r=[],i=w(e),o=i.length-1,a=0;a<=o;a++)n=a===o?this:this.clone(!0),w(i[a])[t](n),s.apply(r,n.get());return this.pushStack(r)}});var We=new RegExp("^("+re+")(?!px)[a-z%]+$","i"),$e=function(t){var n=t.ownerDocument.defaultView;return n&&n.opener||(n=e),n.getComputedStyle(t)},Be=new RegExp(oe.join("|"),"i");!function(){function t(){if(c){l.style.cssText="position:absolute;left:-11111px;width:60px;margin-top:1px;padding:0;border:0",c.style.cssText="position:relative;display:block;box-sizing:border-box;overflow:scroll;margin:auto;border:1px;padding:1px;width:60%;top:1%",be.appendChild(l).appendChild(c);var t=e.getComputedStyle(c);i="1%"!==t.top,u=12===n(t.marginLeft),c.style.right="60%",s=36===n(t.right),o=36===n(t.width),c.style.position="absolute",a=36===c.offsetWidth||"absolute",be.removeChild(l),c=null}}function n(e){return Math.round(parseFloat(e))}var i,o,a,s,u,l=r.createElement("div"),c=r.createElement("div");c.style&&(c.style.backgroundClip="content-box",c.cloneNode(!0).style.backgroundClip="",h.clearCloneStyle="content-box"===c.style.backgroundClip,w.extend(h,{boxSizingReliable:function(){return t(),o},pixelBoxStyles:function(){return t(),s},pixelPosition:function(){return t(),i},reliableMarginLeft:function(){return t(),u},scrollboxSize:function(){return t(),a}}))}();function Fe(e,t,n){var r,i,o,a,s=e.style;return(n=n||$e(e))&&(""!==(a=n.getPropertyValue(t)||n[t])||w.contains(e.ownerDocument,e)||(a=w.style(e,t)),!h.pixelBoxStyles()&&We.test(a)&&Be.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0!==a?a+"":a}function _e(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}var ze=/^(none|table(?!-c[ea]).+)/,Xe=/^--/,Ue={position:"absolute",visibility:"hidden",display:"block"},Ve={letterSpacing:"0",fontWeight:"400"},Ge=["Webkit","Moz","ms"],Ye=r.createElement("div").style;function Qe(e){if(e in Ye)return e;var t=e[0].toUpperCase()+e.slice(1),n=Ge.length;while(n--)if((e=Ge[n]+t)in Ye)return e}function Je(e){var t=w.cssProps[e];return t||(t=w.cssProps[e]=Qe(e)||e),t}function Ke(e,t,n){var r=ie.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function Ze(e,t,n,r,i,o){var a="width"===t?1:0,s=0,u=0;if(n===(r?"border":"content"))return 0;for(;a<4;a+=2)"margin"===n&&(u+=w.css(e,n+oe[a],!0,i)),r?("content"===n&&(u-=w.css(e,"padding"+oe[a],!0,i)),"margin"!==n&&(u-=w.css(e,"border"+oe[a]+"Width",!0,i))):(u+=w.css(e,"padding"+oe[a],!0,i),"padding"!==n?u+=w.css(e,"border"+oe[a]+"Width",!0,i):s+=w.css(e,"border"+oe[a]+"Width",!0,i));return!r&&o>=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a<i;a++)o[t[a]]=w.css(e,t[a],!1,r);return o}return void 0!==n?w.style(e,t,n):w.css(e,t)},e,t,arguments.length>1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o<a;o++)if(r=i[o].call(n,t,e))return r}function ct(e,t,n){var r,i,o,a,s,u,l,c,f="width"in t||"height"in t,p=this,d={},h=e.style,g=e.nodeType&&ae(e),y=J.get(e,"fxshow");n.queue||(null==(a=w._queueHooks(e,"fx")).unqueued&&(a.unqueued=0,s=a.empty.fire,a.empty.fire=function(){a.unqueued||s()}),a.unqueued++,p.always(function(){p.always(function(){a.unqueued--,w.queue(e,"fx").length||a.empty.fire()})}));for(r in t)if(i=t[r],it.test(i)){if(delete t[r],o=o||"toggle"===i,i===(g?"hide":"show")){if("show"!==i||!y||void 0===y[r])continue;g=!0}d[r]=y&&y[r]||w.style(e,r)}if((u=!w.isEmptyObject(t))||!w.isEmptyObject(d)){f&&1===e.nodeType&&(n.overflow=[h.overflow,h.overflowX,h.overflowY],null==(l=y&&y.display)&&(l=J.get(e,"display")),"none"===(c=w.css(e,"display"))&&(l?c=l:(fe([e],!0),l=e.style.display||l,c=w.css(e,"display"),fe([e]))),("inline"===c||"inline-block"===c&&null!=l)&&"none"===w.css(e,"float")&&(u||(p.done(function(){h.display=l}),null==l&&(c=h.display,l="none"===c?"":c)),h.display="inline-block")),n.overflow&&(h.overflow="hidden",p.always(function(){h.overflow=n.overflow[0],h.overflowX=n.overflow[1],h.overflowY=n.overflow[2]})),u=!1;for(r in d)u||(y?"hidden"in y&&(g=y.hidden):y=J.access(e,"fxshow",{display:l}),o&&(y.hidden=!g),g&&fe([e],!0),p.done(function(){g||fe([e]),J.remove(e,"fxshow");for(r in d)w.style(e,r,d[r])})),u=lt(g?y[r]:0,r,p),r in y||(y[r]=u.start,g&&(u.end=u.start,u.start=0))}}function ft(e,t){var n,r,i,o,a;for(n in e)if(r=G(n),i=t[r],o=e[n],Array.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),(a=w.cssHooks[r])&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}function pt(e,t,n){var r,i,o=0,a=pt.prefilters.length,s=w.Deferred().always(function(){delete u.elem}),u=function(){if(i)return!1;for(var t=nt||st(),n=Math.max(0,l.startTime+l.duration-t),r=1-(n/l.duration||0),o=0,a=l.tweens.length;o<a;o++)l.tweens[o].run(r);return s.notifyWith(e,[l,r,n]),r<1&&a?n:(a||s.notifyWith(e,[l,1,0]),s.resolveWith(e,[l]),!1)},l=s.promise({elem:e,props:w.extend({},t),opts:w.extend(!0,{specialEasing:{},easing:w.easing._default},n),originalProperties:t,originalOptions:n,startTime:nt||st(),duration:n.duration,tweens:[],createTween:function(t,n){var r=w.Tween(e,l.opts,t,n,l.opts.specialEasing[t]||l.opts.easing);return l.tweens.push(r),r},stop:function(t){var n=0,r=t?l.tweens.length:0;if(i)return this;for(i=!0;n<r;n++)l.tweens[n].run(1);return t?(s.notifyWith(e,[l,1,0]),s.resolveWith(e,[l,t])):s.rejectWith(e,[l,t]),this}}),c=l.props;for(ft(c,l.opts.specialEasing);o<a;o++)if(r=pt.prefilters[o].call(l,e,c,l.opts))return g(r.stop)&&(w._queueHooks(l.elem,l.opts.queue).stop=r.stop.bind(r)),r;return w.map(c,lt,l),g(l.opts.start)&&l.opts.start.call(e,l),l.progress(l.opts.progress).done(l.opts.done,l.opts.complete).fail(l.opts.fail).always(l.opts.always),w.fx.timer(w.extend(u,{elem:e,anim:l,queue:l.opts.queue})),l}w.Animation=w.extend(pt,{tweeners:{"*":[function(e,t){var n=this.createTween(e,t);return ue(n.elem,e,ie.exec(t),n),n}]},tweener:function(e,t){g(e)?(t=e,e=["*"]):e=e.match(M);for(var n,r=0,i=e.length;r<i;r++)n=e[r],pt.tweeners[n]=pt.tweeners[n]||[],pt.tweeners[n].unshift(t)},prefilters:[ct],prefilter:function(e,t){t?pt.prefilters.unshift(e):pt.prefilters.push(e)}}),w.speed=function(e,t,n){var r=e&&"object"==typeof e?w.extend({},e):{complete:n||!n&&t||g(e)&&e,duration:e,easing:n&&t||t&&!g(t)&&t};return w.fx.off?r.duration=0:"number"!=typeof r.duration&&(r.duration in w.fx.speeds?r.duration=w.fx.speeds[r.duration]:r.duration=w.fx.speeds._default),null!=r.queue&&!0!==r.queue||(r.queue="fx"),r.old=r.complete,r.complete=function(){g(r.old)&&r.old.call(this),r.queue&&w.dequeue(this,r.queue)},r},w.fn.extend({fadeTo:function(e,t,n,r){return this.filter(ae).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=w.isEmptyObject(e),o=w.speed(t,n,r),a=function(){var t=pt(this,w.extend({},e),o);(i||J.get(this,"finish"))&&t.stop(!0)};return a.finish=a,i||!1===o.queue?this.each(a):this.queue(o.queue,a)},stop:function(e,t,n){var r=function(e){var t=e.stop;delete e.stop,t(n)};return"string"!=typeof e&&(n=t,t=e,e=void 0),t&&!1!==e&&this.queue(e||"fx",[]),this.each(function(){var t=!0,i=null!=e&&e+"queueHooks",o=w.timers,a=J.get(this);if(i)a[i]&&a[i].stop&&r(a[i]);else for(i in a)a[i]&&a[i].stop&&ot.test(i)&&r(a[i]);for(i=o.length;i--;)o[i].elem!==this||null!=e&&o[i].queue!==e||(o[i].anim.stop(n),t=!1,o.splice(i,1));!t&&n||w.dequeue(this,e)})},finish:function(e){return!1!==e&&(e=e||"fx"),this.each(function(){var t,n=J.get(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=w.timers,a=r?r.length:0;for(n.finish=!0,w.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;t<a;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}}),w.each(["toggle","show","hide"],function(e,t){var n=w.fn[t];w.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(ut(t,!0),e,r,i)}}),w.each({slideDown:ut("show"),slideUp:ut("hide"),slideToggle:ut("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){w.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),w.timers=[],w.fx.tick=function(){var e,t=0,n=w.timers;for(nt=Date.now();t<n.length;t++)(e=n[t])()||n[t]!==e||n.splice(t--,1);n.length||w.fx.stop(),nt=void 0},w.fx.timer=function(e){w.timers.push(e),w.fx.start()},w.fx.interval=13,w.fx.start=function(){rt||(rt=!0,at())},w.fx.stop=function(){rt=null},w.fx.speeds={slow:600,fast:200,_default:400},w.fn.delay=function(t,n){return t=w.fx?w.fx.speeds[t]||t:t,n=n||"fx",this.queue(n,function(n,r){var i=e.setTimeout(n,t);r.stop=function(){e.clearTimeout(i)}})},function(){var e=r.createElement("input"),t=r.createElement("select").appendChild(r.createElement("option"));e.type="checkbox",h.checkOn=""!==e.value,h.optSelected=t.selected,(e=r.createElement("input")).value="t",e.type="radio",h.radioValue="t"===e.value}();var dt,ht=w.expr.attrHandle;w.fn.extend({attr:function(e,t){return z(this,w.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r<u;r++)if(((n=i[r]).selected||r===o)&&!n.disabled&&(!n.parentNode.disabled||!N(n.parentNode,"optgroup"))){if(t=w(n).val(),a)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=w.makeArray(t),a=i.length;while(a--)((r=i[a]).selected=w.inArray(w.valHooks.option.get(r),o)>-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w("<script>").prop({charset:e.scriptCharset,src:e.url}).on("load error",n=function(e){t.remove(),n=null,e&&o("error"===e.type?404:200,e.type)}),r.head.appendChild(t[0])},abort:function(){n&&n()}}}});var Yt=[],Qt=/(=)\?(?=&|$)|\?\?/;w.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Yt.pop()||w.expando+"_"+Et++;return this[e]=!0,e}}),w.ajaxPrefilter("json jsonp",function(t,n,r){var i,o,a,s=!1!==t.jsonp&&(Qt.test(t.url)?"url":"string"==typeof t.data&&0===(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&Qt.test(t.data)&&"data");if(s||"jsonp"===t.dataTypes[0])return i=t.jsonpCallback=g(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(Qt,"$1"+i):!1!==t.jsonp&&(t.url+=(kt.test(t.url)?"&":"?")+t.jsonp+"="+i),t.converters["script json"]=function(){return a||w.error(i+" was not called"),a[0]},t.dataTypes[0]="json",o=e[i],e[i]=function(){a=arguments},r.always(function(){void 0===o?w(e).removeProp(i):e[i]=o,t[i]&&(t.jsonpCallback=n.jsonpCallback,Yt.push(i)),a&&g(o)&&o(a[0]),a=o=void 0}),"script"}),h.createHTMLDocument=function(){var e=r.implementation.createHTMLDocument("").body;return e.innerHTML="<form></form><form></form>",2===e.childNodes.length}(),w.parseHTML=function(e,t,n){if("string"!=typeof e)return[];"boolean"==typeof t&&(n=t,t=!1);var i,o,a;return t||(h.createHTMLDocument?((i=(t=r.implementation.createHTMLDocument("")).createElement("base")).href=r.location.href,t.head.appendChild(i)):t=r),o=A.exec(e),a=!n&&[],o?[t.createElement(o[1])]:(o=xe([e],t,a),a&&a.length&&w(a).remove(),w.merge([],o.childNodes))},w.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return s>-1&&(r=vt(e.slice(s)),e=e.slice(0,s)),g(t)?(n=t,t=void 0):t&&"object"==typeof t&&(i="POST"),a.length>0&&w.ajax({url:e,type:i||"GET",dataType:"html",data:t}).done(function(e){o=arguments,a.html(r?w("<div>").append(w.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},w.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){w.fn[t]=function(e){return this.on(t,e)}}),w.expr.pseudos.animated=function(e){return w.grep(w.timers,function(t){return e===t.elem}).length},w.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l,c=w.css(e,"position"),f=w(e),p={};"static"===c&&(e.style.position="relative"),s=f.offset(),o=w.css(e,"top"),u=w.css(e,"left"),(l=("absolute"===c||"fixed"===c)&&(o+u).indexOf("auto")>-1)?(a=(r=f.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),g(t)&&(t=t.call(e,n,w.extend({},s))),null!=t.top&&(p.top=t.top-s.top+a),null!=t.left&&(p.left=t.left-s.left+i),"using"in t?t.using.call(e,p):f.css(p)}},w.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each(function(t){w.offset.setOffset(this,e,t)});var t,n,r=this[0];if(r)return r.getClientRects().length?(t=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:t.top+n.pageYOffset,left:t.left+n.pageXOffset}):{top:0,left:0}},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===w.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===w.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=w(e).offset()).top+=w.css(e,"borderTopWidth",!0),i.left+=w.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-w.css(r,"marginTop",!0),left:t.left-i.left-w.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===w.css(e,"position"))e=e.offsetParent;return e||be})}}),w.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,t){var n="pageYOffset"===t;w.fn[e]=function(r){return z(this,function(e,r,i){var o;if(y(e)?o=e:9===e.nodeType&&(o=e.defaultView),void 0===i)return o?o[t]:e[r];o?o.scrollTo(n?o.pageXOffset:i,n?i:o.pageYOffset):e[r]=i},e,r,arguments.length)}}),w.each(["top","left"],function(e,t){w.cssHooks[t]=_e(h.pixelPosition,function(e,n){if(n)return n=Fe(e,t),We.test(n)?w(e).position()[t]+"px":n})}),w.each({Height:"height",Width:"width"},function(e,t){w.each({padding:"inner"+e,content:t,"":"outer"+e},function(n,r){w.fn[r]=function(i,o){var a=arguments.length&&(n||"boolean"!=typeof i),s=n||(!0===i||!0===o?"margin":"border");return z(this,function(t,n,i){var o;return y(t)?0===r.indexOf("outer")?t["inner"+e]:t.document.documentElement["client"+e]:9===t.nodeType?(o=t.documentElement,Math.max(t.body["scroll"+e],o["scroll"+e],t.body["offset"+e],o["offset"+e],o["client"+e])):void 0===i?w.css(t,n,s):w.style(t,n,i,s)},t,a?i:void 0,a)}})}),w.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,t){w.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),w.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),w.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}}),w.proxy=function(e,t){var n,r,i;if("string"==typeof t&&(n=e[t],t=e,e=n),g(e))return r=o.call(arguments,2),i=function(){return e.apply(t||this,r.concat(o.call(arguments)))},i.guid=e.guid=e.guid||w.guid++,i},w.holdReady=function(e){e?w.readyWait++:w.ready(!0)},w.isArray=Array.isArray,w.parseJSON=JSON.parse,w.nodeName=N,w.isFunction=g,w.isWindow=y,w.camelCase=G,w.type=x,w.now=Date.now,w.isNumeric=function(e){var t=w.type(e);return("number"===t||"string"===t)&&!isNaN(e-parseFloat(e))},"function"==typeof define&&define.amd&&define("jquery",[],function(){return w});var Jt=e.jQuery,Kt=e.$;return w.noConflict=function(t){return e.$===w&&(e.$=Kt),t&&e.jQuery===w&&(e.jQuery=Jt),w},t||(e.jQuery=e.$=w),w});
diff --git a/website/themes/lektor-icon/assets/static/js/jquery.easing.min.js b/website/themes/lektor-icon/assets/static/js/jquery.easing.min.js
new file mode 100644
index 0000000..fc7dae4
--- /dev/null
+++ b/website/themes/lektor-icon/assets/static/js/jquery.easing.min.js
@@ -0,0 +1 @@
+!function(n){"function"==typeof define&&define.amd?define(["jquery"],function(e){return n(e)}):"object"==typeof module&&"object"==typeof module.exports?exports=n(require("jquery")):n(jQuery)}(function(n){function e(n){var e=7.5625,t=2.75;return n<1/t?e*n*n:n<2/t?e*(n-=1.5/t)*n+.75:n<2.5/t?e*(n-=2.25/t)*n+.9375:e*(n-=2.625/t)*n+.984375}void 0!==n.easing&&(n.easing.jswing=n.easing.swing);var t=Math.pow,u=Math.sqrt,r=Math.sin,i=Math.cos,a=Math.PI,c=1.70158,o=1.525*c,s=2*a/3,f=2*a/4.5;n.extend(n.easing,{def:"easeOutQuad",swing:function(e){return n.easing[n.easing.def](e)},easeInQuad:function(n){return n*n},easeOutQuad:function(n){return 1-(1-n)*(1-n)},easeInOutQuad:function(n){return n<.5?2*n*n:1-t(-2*n+2,2)/2},easeInCubic:function(n){return n*n*n},easeOutCubic:function(n){return 1-t(1-n,3)},easeInOutCubic:function(n){return n<.5?4*n*n*n:1-t(-2*n+2,3)/2},easeInQuart:function(n){return n*n*n*n},easeOutQuart:function(n){return 1-t(1-n,4)},easeInOutQuart:function(n){return n<.5?8*n*n*n*n:1-t(-2*n+2,4)/2},easeInQuint:function(n){return n*n*n*n*n},easeOutQuint:function(n){return 1-t(1-n,5)},easeInOutQuint:function(n){return n<.5?16*n*n*n*n*n:1-t(-2*n+2,5)/2},easeInSine:function(n){return 1-i(n*a/2)},easeOutSine:function(n){return r(n*a/2)},easeInOutSine:function(n){return-(i(a*n)-1)/2},easeInExpo:function(n){return 0===n?0:t(2,10*n-10)},easeOutExpo:function(n){return 1===n?1:1-t(2,-10*n)},easeInOutExpo:function(n){return 0===n?0:1===n?1:n<.5?t(2,20*n-10)/2:(2-t(2,-20*n+10))/2},easeInCirc:function(n){return 1-u(1-t(n,2))},easeOutCirc:function(n){return u(1-t(n-1,2))},easeInOutCirc:function(n){return n<.5?(1-u(1-t(2*n,2)))/2:(u(1-t(-2*n+2,2))+1)/2},easeInElastic:function(n){return 0===n?0:1===n?1:-t(2,10*n-10)*r((10*n-10.75)*s)},easeOutElastic:function(n){return 0===n?0:1===n?1:t(2,-10*n)*r((10*n-.75)*s)+1},easeInOutElastic:function(n){return 0===n?0:1===n?1:n<.5?-(t(2,20*n-10)*r((20*n-11.125)*f))/2:t(2,-20*n+10)*r((20*n-11.125)*f)/2+1},easeInBack:function(n){return(c+1)*n*n*n-c*n*n},easeOutBack:function(n){return 1+(c+1)*t(n-1,3)+c*t(n-1,2)},easeInOutBack:function(n){return n<.5?t(2*n,2)*(7.189819*n-o)/2:(t(2*n-2,2)*((o+1)*(2*n-2)+o)+2)/2},easeInBounce:function(n){return 1-e(1-n)},easeOutBounce:e,easeInOutBounce:function(n){return n<.5?(1-e(1-2*n))/2:(1+e(2*n-1))/2}})});
diff --git a/website/themes/lektor-icon/assets/static/js/jquery.magnific-popup.min.js b/website/themes/lektor-icon/assets/static/js/jquery.magnific-popup.min.js
new file mode 100644
index 0000000..df42572
--- /dev/null
+++ b/website/themes/lektor-icon/assets/static/js/jquery.magnific-popup.min.js
@@ -0,0 +1,4 @@
+/*! Magnific Popup - v1.1.0 - 2016-02-20
+* http://dimsemenov.com/plugins/magnific-popup/
+* Copyright (c) 2016 Dmitry Semenov; */
+!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):window.jQuery||window.Zepto)}(function(a){var b,c,d,e,f,g,h="Close",i="BeforeClose",j="AfterClose",k="BeforeAppend",l="MarkupParse",m="Open",n="Change",o="mfp",p="."+o,q="mfp-ready",r="mfp-removing",s="mfp-prevent-close",t=function(){},u=!!window.jQuery,v=a(window),w=function(a,c){b.ev.on(o+a+p,c)},x=function(b,c,d,e){var f=document.createElement("div");return f.className="mfp-"+b,d&&(f.innerHTML=d),e?c&&c.appendChild(f):(f=a(f),c&&f.appendTo(c)),f},y=function(c,d){b.ev.triggerHandler(o+c,d),b.st.callbacks&&(c=c.charAt(0).toLowerCase()+c.slice(1),b.st.callbacks[c]&&b.st.callbacks[c].apply(b,a.isArray(d)?d:[d]))},z=function(c){return c===g&&b.currTemplate.closeBtn||(b.currTemplate.closeBtn=a(b.st.closeMarkup.replace("%title%",b.st.tClose)),g=c),b.currTemplate.closeBtn},A=function(){a.magnificPopup.instance||(b=new t,b.init(),a.magnificPopup.instance=b)},B=function(){var a=document.createElement("p").style,b=["ms","O","Moz","Webkit"];if(void 0!==a.transition)return!0;for(;b.length;)if(b.pop()+"Transition"in a)return!0;return!1};t.prototype={constructor:t,init:function(){var c=navigator.appVersion;b.isLowIE=b.isIE8=document.all&&!document.addEventListener,b.isAndroid=/android/gi.test(c),b.isIOS=/iphone|ipad|ipod/gi.test(c),b.supportsTransition=B(),b.probablyMobile=b.isAndroid||b.isIOS||/(Opera Mini)|Kindle|webOS|BlackBerry|(Opera Mobi)|(Windows Phone)|IEMobile/i.test(navigator.userAgent),d=a(document),b.popupsCache={}},open:function(c){var e;if(c.isObj===!1){b.items=c.items.toArray(),b.index=0;var g,h=c.items;for(e=0;e<h.length;e++)if(g=h[e],g.parsed&&(g=g.el[0]),g===c.el[0]){b.index=e;break}}else b.items=a.isArray(c.items)?c.items:[c.items],b.index=c.index||0;if(b.isOpen)return void b.updateItemHTML();b.types=[],f="",c.mainEl&&c.mainEl.length?b.ev=c.mainEl.eq(0):b.ev=d,c.key?(b.popupsCache[c.key]||(b.popupsCache[c.key]={}),b.currTemplate=b.popupsCache[c.key]):b.currTemplate={},b.st=a.extend(!0,{},a.magnificPopup.defaults,c),b.fixedContentPos="auto"===b.st.fixedContentPos?!b.probablyMobile:b.st.fixedContentPos,b.st.modal&&(b.st.closeOnContentClick=!1,b.st.closeOnBgClick=!1,b.st.showCloseBtn=!1,b.st.enableEscapeKey=!1),b.bgOverlay||(b.bgOverlay=x("bg").on("click"+p,function(){b.close()}),b.wrap=x("wrap").attr("tabindex",-1).on("click"+p,function(a){b._checkIfClose(a.target)&&b.close()}),b.container=x("container",b.wrap)),b.contentContainer=x("content"),b.st.preloader&&(b.preloader=x("preloader",b.container,b.st.tLoading));var i=a.magnificPopup.modules;for(e=0;e<i.length;e++){var j=i[e];j=j.charAt(0).toUpperCase()+j.slice(1),b["init"+j].call(b)}y("BeforeOpen"),b.st.showCloseBtn&&(b.st.closeBtnInside?(w(l,function(a,b,c,d){c.close_replaceWith=z(d.type)}),f+=" mfp-close-btn-in"):b.wrap.append(z())),b.st.alignTop&&(f+=" mfp-align-top"),b.fixedContentPos?b.wrap.css({overflow:b.st.overflowY,overflowX:"hidden",overflowY:b.st.overflowY}):b.wrap.css({top:v.scrollTop(),position:"absolute"}),(b.st.fixedBgPos===!1||"auto"===b.st.fixedBgPos&&!b.fixedContentPos)&&b.bgOverlay.css({height:d.height(),position:"absolute"}),b.st.enableEscapeKey&&d.on("keyup"+p,function(a){27===a.keyCode&&b.close()}),v.on("resize"+p,function(){b.updateSize()}),b.st.closeOnContentClick||(f+=" mfp-auto-cursor"),f&&b.wrap.addClass(f);var k=b.wH=v.height(),n={};if(b.fixedContentPos&&b._hasScrollBar(k)){var o=b._getScrollbarSize();o&&(n.marginRight=o)}b.fixedContentPos&&(b.isIE7?a("body, html").css("overflow","hidden"):n.overflow="hidden");var r=b.st.mainClass;return b.isIE7&&(r+=" mfp-ie7"),r&&b._addClassToMFP(r),b.updateItemHTML(),y("BuildControls"),a("html").css(n),b.bgOverlay.add(b.wrap).prependTo(b.st.prependTo||a(document.body)),b._lastFocusedEl=document.activeElement,setTimeout(function(){b.content?(b._addClassToMFP(q),b._setFocus()):b.bgOverlay.addClass(q),d.on("focusin"+p,b._onFocusIn)},16),b.isOpen=!0,b.updateSize(k),y(m),c},close:function(){b.isOpen&&(y(i),b.isOpen=!1,b.st.removalDelay&&!b.isLowIE&&b.supportsTransition?(b._addClassToMFP(r),setTimeout(function(){b._close()},b.st.removalDelay)):b._close())},_close:function(){y(h);var c=r+" "+q+" ";if(b.bgOverlay.detach(),b.wrap.detach(),b.container.empty(),b.st.mainClass&&(c+=b.st.mainClass+" "),b._removeClassFromMFP(c),b.fixedContentPos){var e={marginRight:""};b.isIE7?a("body, html").css("overflow",""):e.overflow="",a("html").css(e)}d.off("keyup"+p+" focusin"+p),b.ev.off(p),b.wrap.attr("class","mfp-wrap").removeAttr("style"),b.bgOverlay.attr("class","mfp-bg"),b.container.attr("class","mfp-container"),!b.st.showCloseBtn||b.st.closeBtnInside&&b.currTemplate[b.currItem.type]!==!0||b.currTemplate.closeBtn&&b.currTemplate.closeBtn.detach(),b.st.autoFocusLast&&b._lastFocusedEl&&a(b._lastFocusedEl).on('focus'),b.currItem=null,b.content=null,b.currTemplate=null,b.prevHeight=0,y(j)},updateSize:function(a){if(b.isIOS){var c=document.documentElement.clientWidth/window.innerWidth,d=window.innerHeight*c;b.wrap.css("height",d),b.wH=d}else b.wH=a||v.height();b.fixedContentPos||b.wrap.css("height",b.wH),y("Resize")},updateItemHTML:function(){var c=b.items[b.index];b.contentContainer.detach(),b.content&&b.content.detach(),c.parsed||(c=b.parseEl(b.index));var d=c.type;if(y("BeforeChange",[b.currItem?b.currItem.type:"",d]),b.currItem=c,!b.currTemplate[d]){var f=b.st[d]?b.st[d].markup:!1;y("FirstMarkupParse",f),f?b.currTemplate[d]=a(f):b.currTemplate[d]=!0}e&&e!==c.type&&b.container.removeClass("mfp-"+e+"-holder");var g=b["get"+d.charAt(0).toUpperCase()+d.slice(1)](c,b.currTemplate[d]);b.appendContent(g,d),c.preloaded=!0,y(n,c),e=c.type,b.container.prepend(b.contentContainer),y("AfterChange")},appendContent:function(a,c){b.content=a,a?b.st.showCloseBtn&&b.st.closeBtnInside&&b.currTemplate[c]===!0?b.content.find(".mfp-close").length||b.content.append(z()):b.content=a:b.content="",y(k),b.container.addClass("mfp-"+c+"-holder"),b.contentContainer.append(b.content)},parseEl:function(c){var d,e=b.items[c];if(e.tagName?e={el:a(e)}:(d=e.type,e={data:e,src:e.src}),e.el){for(var f=b.types,g=0;g<f.length;g++)if(e.el.hasClass("mfp-"+f[g])){d=f[g];break}e.src=e.el.attr("data-mfp-src"),e.src||(e.src=e.el.attr("href"))}return e.type=d||b.st.type||"inline",e.index=c,e.parsed=!0,b.items[c]=e,y("ElementParse",e),b.items[c]},addGroup:function(a,c){var d=function(d){d.mfpEl=this,b._openClick(d,a,c)};c||(c={});var e="click.magnificPopup";c.mainEl=a,c.items?(c.isObj=!0,a.off(e).on(e,d)):(c.isObj=!1,c.delegate?a.off(e).on(e,c.delegate,d):(c.items=a,a.off(e).on(e,d)))},_openClick:function(c,d,e){var f=void 0!==e.midClick?e.midClick:a.magnificPopup.defaults.midClick;if(f||!(2===c.which||c.ctrlKey||c.metaKey||c.altKey||c.shiftKey)){var g=void 0!==e.disableOn?e.disableOn:a.magnificPopup.defaults.disableOn;if(g)if(a.isFunction(g)){if(!g.call(b))return!0}else if(v.width()<g)return!0;c.type&&(c.preventDefault(),b.isOpen&&c.stopPropagation()),e.el=a(c.mfpEl),e.delegate&&(e.items=d.find(e.delegate)),b.open(e)}},updateStatus:function(a,d){if(b.preloader){c!==a&&b.container.removeClass("mfp-s-"+c),d||"loading"!==a||(d=b.st.tLoading);var e={status:a,text:d};y("UpdateStatus",e),a=e.status,d=e.text,b.preloader.html(d),b.preloader.find("a").on("click",function(a){a.stopImmediatePropagation()}),b.container.addClass("mfp-s-"+a),c=a}},_checkIfClose:function(c){if(!a(c).hasClass(s)){var d=b.st.closeOnContentClick,e=b.st.closeOnBgClick;if(d&&e)return!0;if(!b.content||a(c).hasClass("mfp-close")||b.preloader&&c===b.preloader[0])return!0;if(c===b.content[0]||a.contains(b.content[0],c)){if(d)return!0}else if(e&&a.contains(document,c))return!0;return!1}},_addClassToMFP:function(a){b.bgOverlay.addClass(a),b.wrap.addClass(a)},_removeClassFromMFP:function(a){this.bgOverlay.removeClass(a),b.wrap.removeClass(a)},_hasScrollBar:function(a){return(b.isIE7?d.height():document.body.scrollHeight)>(a||v.height())},_setFocus:function(){(b.st.focus?b.content.find(b.st.focus).eq(0):b.wrap).on('focus')},_onFocusIn:function(c){return c.target===b.wrap[0]||a.contains(b.wrap[0],c.target)?void 0:(b._setFocus(),!1)},_parseMarkup:function(b,c,d){var e;d.data&&(c=a.extend(d.data,c)),y(l,[b,c,d]),a.each(c,function(c,d){if(void 0===d||d===!1)return!0;if(e=c.split("_"),e.length>1){var f=b.find(p+"-"+e[0]);if(f.length>0){var g=e[1];"replaceWith"===g?f[0]!==d[0]&&f.replaceWith(d):"img"===g?f.is("img")?f.attr("src",d):f.replaceWith(a("<img>").attr("src",d).attr("class",f.attr("class"))):f.attr(e[1],d)}}else b.find(p+"-"+c).html(d)})},_getScrollbarSize:function(){if(void 0===b.scrollbarSize){var a=document.createElement("div");a.style.cssText="width: 99px; height: 99px; overflow: scroll; position: absolute; top: -9999px;",document.body.appendChild(a),b.scrollbarSize=a.offsetWidth-a.clientWidth,document.body.removeChild(a)}return b.scrollbarSize}},a.magnificPopup={instance:null,proto:t.prototype,modules:[],open:function(b,c){return A(),b=b?a.extend(!0,{},b):{},b.isObj=!0,b.index=c||0,this.instance.open(b)},close:function(){return a.magnificPopup.instance&&a.magnificPopup.instance.close()},registerModule:function(b,c){c.options&&(a.magnificPopup.defaults[b]=c.options),a.extend(this.proto,c.proto),this.modules.push(b)},defaults:{disableOn:0,key:null,midClick:!1,mainClass:"",preloader:!0,focus:"",closeOnContentClick:!1,closeOnBgClick:!0,closeBtnInside:!0,showCloseBtn:!0,enableEscapeKey:!0,modal:!1,alignTop:!1,removalDelay:0,prependTo:null,fixedContentPos:"auto",fixedBgPos:"auto",overflowY:"auto",closeMarkup:'<button title="%title%" type="button" class="mfp-close">&#215;</button>',tClose:"Close (Esc)",tLoading:"Loading...",autoFocusLast:!0}},a.fn.magnificPopup=function(c){A();var d=a(this);if("string"==typeof c)if("open"===c){var e,f=u?d.data("magnificPopup"):d[0].magnificPopup,g=parseInt(arguments[1],10)||0;f.items?e=f.items[g]:(e=d,f.delegate&&(e=e.find(f.delegate)),e=e.eq(g)),b._openClick({mfpEl:e},d,f)}else b.isOpen&&b[c].apply(b,Array.prototype.slice.call(arguments,1));else c=a.extend(!0,{},c),u?d.data("magnificPopup",c):d[0].magnificPopup=c,b.addGroup(d,c);return d};var C,D,E,F="inline",G=function(){E&&(D.after(E.addClass(C)).detach(),E=null)};a.magnificPopup.registerModule(F,{options:{hiddenClass:"hide",markup:"",tNotFound:"Content not found"},proto:{initInline:function(){b.types.push(F),w(h+"."+F,function(){G()})},getInline:function(c,d){if(G(),c.src){var e=b.st.inline,f=a(c.src);if(f.length){var g=f[0].parentNode;g&&g.tagName&&(D||(C=e.hiddenClass,D=x(C),C="mfp-"+C),E=f.after(D).detach().removeClass(C)),b.updateStatus("ready")}else b.updateStatus("error",e.tNotFound),f=a("<div>");return c.inlineElement=f,f}return b.updateStatus("ready"),b._parseMarkup(d,{},c),d}}});var H,I="ajax",J=function(){H&&a(document.body).removeClass(H)},K=function(){J(),b.req&&b.req.abort()};a.magnificPopup.registerModule(I,{options:{settings:null,cursor:"mfp-ajax-cur",tError:'<a href="%url%">The content</a> could not be loaded.'},proto:{initAjax:function(){b.types.push(I),H=b.st.ajax.cursor,w(h+"."+I,K),w("BeforeChange."+I,K)},getAjax:function(c){H&&a(document.body).addClass(H),b.updateStatus("loading");var d=a.extend({url:c.src,success:function(d,e,f){var g={data:d,xhr:f};y("ParseAjax",g),b.appendContent(a(g.data),I),c.finished=!0,J(),b._setFocus(),setTimeout(function(){b.wrap.addClass(q)},16),b.updateStatus("ready"),y("AjaxContentAdded")},error:function(){J(),c.finished=c.loadError=!0,b.updateStatus("error",b.st.ajax.tError.replace("%url%",c.src))}},b.st.ajax.settings);return b.req=a.ajax(d),""}}});var L,M=function(c){if(c.data&&void 0!==c.data.title)return c.data.title;var d=b.st.image.titleSrc;if(d){if(a.isFunction(d))return d.call(b,c);if(c.el)return c.el.attr(d)||""}return""};a.magnificPopup.registerModule("image",{options:{markup:'<div class="mfp-figure"><div class="mfp-close"></div><figure><div class="mfp-img"></div><figcaption><div class="mfp-bottom-bar"><div class="mfp-title"></div><div class="mfp-counter"></div></div></figcaption></figure></div>',cursor:"mfp-zoom-out-cur",titleSrc:"title",verticalFit:!0,tError:'<a href="%url%">The image</a> could not be loaded.'},proto:{initImage:function(){var c=b.st.image,d=".image";b.types.push("image"),w(m+d,function(){"image"===b.currItem.type&&c.cursor&&a(document.body).addClass(c.cursor)}),w(h+d,function(){c.cursor&&a(document.body).removeClass(c.cursor),v.off("resize"+p)}),w("Resize"+d,b.resizeImage),b.isLowIE&&w("AfterChange",b.resizeImage)},resizeImage:function(){var a=b.currItem;if(a&&a.img&&b.st.image.verticalFit){var c=0;b.isLowIE&&(c=parseInt(a.img.css("padding-top"),10)+parseInt(a.img.css("padding-bottom"),10)),a.img.css("max-height",b.wH-c)}},_onImageHasSize:function(a){a.img&&(a.hasSize=!0,L&&clearInterval(L),a.isCheckingImgSize=!1,y("ImageHasSize",a),a.imgHidden&&(b.content&&b.content.removeClass("mfp-loading"),a.imgHidden=!1))},findImageSize:function(a){var c=0,d=a.img[0],e=function(f){L&&clearInterval(L),L=setInterval(function(){return d.naturalWidth>0?void b._onImageHasSize(a):(c>200&&clearInterval(L),c++,void(3===c?e(10):40===c?e(50):100===c&&e(500)))},f)};e(1)},getImage:function(c,d){var e=0,f=function(){c&&(c.img[0].complete?(c.img.off(".mfploader"),c===b.currItem&&(b._onImageHasSize(c),b.updateStatus("ready")),c.hasSize=!0,c.loaded=!0,y("ImageLoadComplete")):(e++,200>e?setTimeout(f,100):g()))},g=function(){c&&(c.img.off(".mfploader"),c===b.currItem&&(b._onImageHasSize(c),b.updateStatus("error",h.tError.replace("%url%",c.src))),c.hasSize=!0,c.loaded=!0,c.loadError=!0)},h=b.st.image,i=d.find(".mfp-img");if(i.length){var j=document.createElement("img");j.className="mfp-img",c.el&&c.el.find("img").length&&(j.alt=c.el.find("img").attr("alt")),c.img=a(j).on("load.mfploader",f).on("error.mfploader",g),j.src=c.src,i.is("img")&&(c.img=c.img.clone()),j=c.img[0],j.naturalWidth>0?c.hasSize=!0:j.width||(c.hasSize=!1)}return b._parseMarkup(d,{title:M(c),img_replaceWith:c.img},c),b.resizeImage(),c.hasSize?(L&&clearInterval(L),c.loadError?(d.addClass("mfp-loading"),b.updateStatus("error",h.tError.replace("%url%",c.src))):(d.removeClass("mfp-loading"),b.updateStatus("ready")),d):(b.updateStatus("loading"),c.loading=!0,c.hasSize||(c.imgHidden=!0,d.addClass("mfp-loading"),b.findImageSize(c)),d)}}});var N,O=function(){return void 0===N&&(N=void 0!==document.createElement("p").style.MozTransform),N};a.magnificPopup.registerModule("zoom",{options:{enabled:!1,easing:"ease-in-out",duration:300,opener:function(a){return a.is("img")?a:a.find("img")}},proto:{initZoom:function(){var a,c=b.st.zoom,d=".zoom";if(c.enabled&&b.supportsTransition){var e,f,g=c.duration,j=function(a){var b=a.clone().removeAttr("style").removeAttr("class").addClass("mfp-animated-image"),d="all "+c.duration/1e3+"s "+c.easing,e={position:"fixed",zIndex:9999,left:0,top:0,"-webkit-backface-visibility":"hidden"},f="transition";return e["-webkit-"+f]=e["-moz-"+f]=e["-o-"+f]=e[f]=d,b.css(e),b},k=function(){b.content.css("visibility","visible")};w("BuildControls"+d,function(){if(b._allowZoom()){if(clearTimeout(e),b.content.css("visibility","hidden"),a=b._getItemToZoom(),!a)return void k();f=j(a),f.css(b._getOffset()),b.wrap.append(f),e=setTimeout(function(){f.css(b._getOffset(!0)),e=setTimeout(function(){k(),setTimeout(function(){f.remove(),a=f=null,y("ZoomAnimationEnded")},16)},g)},16)}}),w(i+d,function(){if(b._allowZoom()){if(clearTimeout(e),b.st.removalDelay=g,!a){if(a=b._getItemToZoom(),!a)return;f=j(a)}f.css(b._getOffset(!0)),b.wrap.append(f),b.content.css("visibility","hidden"),setTimeout(function(){f.css(b._getOffset())},16)}}),w(h+d,function(){b._allowZoom()&&(k(),f&&f.remove(),a=null)})}},_allowZoom:function(){return"image"===b.currItem.type},_getItemToZoom:function(){return b.currItem.hasSize?b.currItem.img:!1},_getOffset:function(c){var d;d=c?b.currItem.img:b.st.zoom.opener(b.currItem.el||b.currItem);var e=d.offset(),f=parseInt(d.css("padding-top"),10),g=parseInt(d.css("padding-bottom"),10);e.top-=a(window).scrollTop()-f;var h={width:d.width(),height:(u?d.innerHeight():d[0].offsetHeight)-g-f};return O()?h["-moz-transform"]=h.transform="translate("+e.left+"px,"+e.top+"px)":(h.left=e.left,h.top=e.top),h}}});var P="iframe",Q="//about:blank",R=function(a){if(b.currTemplate[P]){var c=b.currTemplate[P].find("iframe");c.length&&(a||(c[0].src=Q),b.isIE8&&c.css("display",a?"block":"none"))}};a.magnificPopup.registerModule(P,{options:{markup:'<div class="mfp-iframe-scaler"><div class="mfp-close"></div><iframe class="mfp-iframe" src="//about:blank" frameborder="0" allowfullscreen></iframe></div>',srcAction:"iframe_src",patterns:{youtube:{index:"youtube.com",id:"v=",src:"//www.youtube.com/embed/%id%?autoplay=1"},vimeo:{index:"vimeo.com/",id:"/",src:"//player.vimeo.com/video/%id%?autoplay=1"},gmaps:{index:"//maps.google.",src:"%id%&output=embed"}}},proto:{initIframe:function(){b.types.push(P),w("BeforeChange",function(a,b,c){b!==c&&(b===P?R():c===P&&R(!0))}),w(h+"."+P,function(){R()})},getIframe:function(c,d){var e=c.src,f=b.st.iframe;a.each(f.patterns,function(){return e.indexOf(this.index)>-1?(this.id&&(e="string"==typeof this.id?e.substr(e.lastIndexOf(this.id)+this.id.length,e.length):this.id.call(this,e)),e=this.src.replace("%id%",e),!1):void 0});var g={};return f.srcAction&&(g[f.srcAction]=e),b._parseMarkup(d,g,c),b.updateStatus("ready"),d}}});var S=function(a){var c=b.items.length;return a>c-1?a-c:0>a?c+a:a},T=function(a,b,c){return a.replace(/%curr%/gi,b+1).replace(/%total%/gi,c)};a.magnificPopup.registerModule("gallery",{options:{enabled:!1,arrowMarkup:'<button title="%title%" type="button" class="mfp-arrow mfp-arrow-%dir%"></button>',preload:[0,2],navigateByImgClick:!0,arrows:!0,tPrev:"Previous (Left arrow key)",tNext:"Next (Right arrow key)",tCounter:"%curr% of %total%"},proto:{initGallery:function(){var c=b.st.gallery,e=".mfp-gallery";return b.direction=!0,c&&c.enabled?(f+=" mfp-gallery",w(m+e,function(){c.navigateByImgClick&&b.wrap.on("click"+e,".mfp-img",function(){return b.items.length>1?(b.next(),!1):void 0}),d.on("keydown"+e,function(a){37===a.keyCode?b.prev():39===a.keyCode&&b.next()})}),w("UpdateStatus"+e,function(a,c){c.text&&(c.text=T(c.text,b.currItem.index,b.items.length))}),w(l+e,function(a,d,e,f){var g=b.items.length;e.counter=g>1?T(c.tCounter,f.index,g):""}),w("BuildControls"+e,function(){if(b.items.length>1&&c.arrows&&!b.arrowLeft){var d=c.arrowMarkup,e=b.arrowLeft=a(d.replace(/%title%/gi,c.tPrev).replace(/%dir%/gi,"left")).addClass(s),f=b.arrowRight=a(d.replace(/%title%/gi,c.tNext).replace(/%dir%/gi,"right")).addClass(s);e.on('click',function(){b.prev()}),f.on('click',function(){b.next()}),b.container.append(e.add(f))}}),w(n+e,function(){b._preloadTimeout&&clearTimeout(b._preloadTimeout),b._preloadTimeout=setTimeout(function(){b.preloadNearbyImages(),b._preloadTimeout=null},16)}),void w(h+e,function(){d.off(e),b.wrap.off("click"+e),b.arrowRight=b.arrowLeft=null})):!1},next:function(){b.direction=!0,b.index=S(b.index+1),b.updateItemHTML()},prev:function(){b.direction=!1,b.index=S(b.index-1),b.updateItemHTML()},goTo:function(a){b.direction=a>=b.index,b.index=a,b.updateItemHTML()},preloadNearbyImages:function(){var a,c=b.st.gallery.preload,d=Math.min(c[0],b.items.length),e=Math.min(c[1],b.items.length);for(a=1;a<=(b.direction?e:d);a++)b._preloadItem(b.index+a);for(a=1;a<=(b.direction?d:e);a++)b._preloadItem(b.index-a)},_preloadItem:function(c){if(c=S(c),!b.items[c].preloaded){var d=b.items[c];d.parsed||(d=b.parseEl(c)),y("LazyLoad",d),"image"===d.type&&(d.img=a('<img class="mfp-img" />').on("load.mfploader",function(){d.hasSize=!0}).on("error.mfploader",function(){d.hasSize=!0,d.loadError=!0,y("LazyLoadError",d)}).attr("src",d.src)),d.preloaded=!0}}}});var U="retina";a.magnificPopup.registerModule(U,{options:{replaceSrc:function(a){return a.src.replace(/\.\w+$/,function(a){return"@2x"+a})},ratio:1},proto:{initRetina:function(){if(window.devicePixelRatio>1){var a=b.st.retina,c=a.ratio;c=isNaN(c)?c():c,c>1&&(w("ImageHasSize."+U,function(a,b){b.img.css({"max-width":b.img[0].naturalWidth/c,width:"100%"})}),w("ElementParse."+U,function(b,d){d.src=a.replaceSrc(d,c)}))}}}}),A()});
diff --git a/website/themes/lektor-icon/assets/static/js/jquery.stellar.js b/website/themes/lektor-icon/assets/static/js/jquery.stellar.js
new file mode 100644
index 0000000..6298b6c
--- /dev/null
+++ b/website/themes/lektor-icon/assets/static/js/jquery.stellar.js
@@ -0,0 +1,660 @@
+/*!
+ * Stellar.js v0.6.2
+ * https://markdalgleish.com/projects/stellar.js
+ *
+ * Copyright 2014, Mark Dalgleish
+ * This content is released under the MIT license
+ * https://markdalgleish.mit-license.org
+ */
+
+;(function($, window, document, undefined) {
+
+	var pluginName = 'stellar',
+		defaults = {
+			scrollProperty: 'scroll',
+			positionProperty: 'position',
+			horizontalScrolling: true,
+			verticalScrolling: true,
+			horizontalOffset: 0,
+			verticalOffset: 0,
+			responsive: false,
+			parallaxBackgrounds: true,
+			parallaxElements: true,
+			hideDistantElements: true,
+			hideElement: function($elem) { $elem.hide(); },
+			showElement: function($elem) { $elem.show(); }
+		},
+
+		scrollProperty = {
+			scroll: {
+				getLeft: function($elem) { return $elem.scrollLeft(); },
+				setLeft: function($elem, val) { $elem.scrollLeft(val); },
+
+				getTop: function($elem) { return $elem.scrollTop();	},
+				setTop: function($elem, val) { $elem.scrollTop(val); }
+			},
+			position: {
+				getLeft: function($elem) { return parseInt($elem.css('left'), 10) * -1; },
+				getTop: function($elem) { return parseInt($elem.css('top'), 10) * -1; }
+			},
+			margin: {
+				getLeft: function($elem) { return parseInt($elem.css('margin-left'), 10) * -1; },
+				getTop: function($elem) { return parseInt($elem.css('margin-top'), 10) * -1; }
+			},
+			transform: {
+				getLeft: function($elem) {
+					var computedTransform = getComputedStyle($elem[0])[prefixedTransform];
+					return (computedTransform !== 'none' ? parseInt(computedTransform.match(/(-?[0-9]+)/g)[4], 10) * -1 : 0);
+				},
+				getTop: function($elem) {
+					var computedTransform = getComputedStyle($elem[0])[prefixedTransform];
+					return (computedTransform !== 'none' ? parseInt(computedTransform.match(/(-?[0-9]+)/g)[5], 10) * -1 : 0);
+				}
+			}
+		},
+
+		positionProperty = {
+			position: {
+				setLeft: function($elem, left) { $elem.css('left', left); },
+				setTop: function($elem, top) { $elem.css('top', top); }
+			},
+			transform: {
+				setPosition: function($elem, left, startingLeft, top, startingTop) {
+					$elem[0].style[prefixedTransform] = 'translate3d(' + (left - startingLeft) + 'px, ' + (top - startingTop) + 'px, 0)';
+				}
+			}
+		},
+
+		// Returns a function which adds a vendor prefix to any CSS property name
+		vendorPrefix = (function() {
+			var prefixes = /^(Moz|Webkit|Khtml|O|ms|Icab)(?=[A-Z])/,
+				style = $('script')[0].style,
+				prefix = '',
+				prop;
+
+			for (prop in style) {
+				if (prefixes.test(prop)) {
+					prefix = prop.match(prefixes)[0];
+					break;
+				}
+			}
+
+			if ('WebkitOpacity' in style) { prefix = 'Webkit'; }
+			if ('KhtmlOpacity' in style) { prefix = 'Khtml'; }
+
+			return function(property) {
+				return prefix + (prefix.length > 0 ? property.charAt(0).toUpperCase() + property.slice(1) : property);
+			};
+		}()),
+
+		prefixedTransform = vendorPrefix('transform'),
+
+		supportsBackgroundPositionXY = $('<div />', { style: 'background:#fff' }).css('background-position-x') !== undefined,
+
+		setBackgroundPosition = (supportsBackgroundPositionXY ?
+			function($elem, x, y) {
+				$elem.css({
+					'background-position-x': x,
+					'background-position-y': y
+				});
+			} :
+			function($elem, x, y) {
+				$elem.css('background-position', x + ' ' + y);
+			}
+		),
+
+		getBackgroundPosition = (supportsBackgroundPositionXY ?
+			function($elem) {
+				return [
+					$elem.css('background-position-x'),
+					$elem.css('background-position-y')
+				];
+			} :
+			function($elem) {
+				return $elem.css('background-position').split(' ');
+			}
+		),
+
+		requestAnimFrame = (
+			window.requestAnimationFrame       ||
+			window.webkitRequestAnimationFrame ||
+			window.mozRequestAnimationFrame    ||
+			window.oRequestAnimationFrame      ||
+			window.msRequestAnimationFrame     ||
+			function(callback) {
+				setTimeout(callback, 1000 / 60);
+			}
+		);
+
+	function Plugin(element, options) {
+		this.element = element;
+		this.options = $.extend({}, defaults, options);
+
+		this._defaults = defaults;
+		this._name = pluginName;
+
+		this.init();
+	}
+
+	Plugin.prototype = {
+		init: function() {
+			this.options.name = pluginName + '_' + Math.floor(Math.random() * 1e9);
+
+			this._defineElements();
+			this._defineGetters();
+			this._defineSetters();
+			this._handleWindowLoadAndResize();
+			this._detectViewport();
+
+			this.refresh({ firstLoad: true });
+
+			if (this.options.scrollProperty === 'scroll') {
+				this._handleScrollEvent();
+			} else {
+				this._startAnimationLoop();
+			}
+		},
+		_defineElements: function() {
+			if (this.element === document.body) this.element = window;
+			this.$scrollElement = $(this.element);
+			this.$element = (this.element === window ? $('body') : this.$scrollElement);
+			this.$viewportElement = (this.options.viewportElement !== undefined ? $(this.options.viewportElement) : (this.$scrollElement[0] === window || this.options.scrollProperty === 'scroll' ? this.$scrollElement : this.$scrollElement.parent()) );
+		},
+		_defineGetters: function() {
+			var self = this,
+				scrollPropertyAdapter = scrollProperty[self.options.scrollProperty];
+
+			this._getScrollLeft = function() {
+				return scrollPropertyAdapter.getLeft(self.$scrollElement);
+			};
+
+			this._getScrollTop = function() {
+				return scrollPropertyAdapter.getTop(self.$scrollElement);
+			};
+		},
+		_defineSetters: function() {
+			var self = this,
+				scrollPropertyAdapter = scrollProperty[self.options.scrollProperty],
+				positionPropertyAdapter = positionProperty[self.options.positionProperty],
+				setScrollLeft = scrollPropertyAdapter.setLeft,
+				setScrollTop = scrollPropertyAdapter.setTop;
+
+			this._setScrollLeft = (typeof setScrollLeft === 'function' ? function(val) {
+				setScrollLeft(self.$scrollElement, val);
+			} : $.noop);
+
+			this._setScrollTop = (typeof setScrollTop === 'function' ? function(val) {
+				setScrollTop(self.$scrollElement, val);
+			} : $.noop);
+
+			this._setPosition = positionPropertyAdapter.setPosition ||
+				function($elem, left, startingLeft, top, startingTop) {
+					if (self.options.horizontalScrolling) {
+						positionPropertyAdapter.setLeft($elem, left, startingLeft);
+					}
+
+					if (self.options.verticalScrolling) {
+						positionPropertyAdapter.setTop($elem, top, startingTop);
+					}
+				};
+		},
+		_handleWindowLoadAndResize: function() {
+			var self = this,
+				$window = $(window);
+
+			if (self.options.responsive) {
+				$window.on('load.' + this.name, function() {
+					self.refresh();
+				});
+			}
+
+			$window.on('resize.' + this.name, function() {
+				self._detectViewport();
+
+				if (self.options.responsive) {
+					self.refresh();
+				}
+			});
+		},
+		refresh: function(options) {
+			var self = this,
+				oldLeft = self._getScrollLeft(),
+				oldTop = self._getScrollTop();
+
+			if (!options || !options.firstLoad) {
+				this._reset();
+			}
+
+			this._setScrollLeft(0);
+			this._setScrollTop(0);
+
+			this._setOffsets();
+			this._findParticles();
+			this._findBackgrounds();
+
+			// Fix for WebKit background rendering bug
+			if (options && options.firstLoad && /WebKit/.test(navigator.userAgent)) {
+				$(window).on('load', function() {
+					var oldLeft = self._getScrollLeft(),
+						oldTop = self._getScrollTop();
+
+					self._setScrollLeft(oldLeft + 1);
+					self._setScrollTop(oldTop + 1);
+
+					self._setScrollLeft(oldLeft);
+					self._setScrollTop(oldTop);
+				});
+			}
+
+			this._setScrollLeft(oldLeft);
+			this._setScrollTop(oldTop);
+		},
+		_detectViewport: function() {
+			var viewportOffsets = this.$viewportElement[0] !== window ? this.$viewportElement.offset() : {top: 0, left: 0},
+				hasOffsets = viewportOffsets !== null && viewportOffsets !== undefined;
+
+			this.viewportWidth = this.$viewportElement.width();
+			this.viewportHeight = this.$viewportElement.height();
+
+			this.viewportOffsetTop = (hasOffsets ? viewportOffsets.top : 0);
+			this.viewportOffsetLeft = (hasOffsets ? viewportOffsets.left : 0);
+		},
+		_findParticles: function() {
+			var self = this,
+				scrollLeft = this._getScrollLeft(),
+				scrollTop = this._getScrollTop();
+
+			if (this.particles !== undefined) {
+				for (var i = this.particles.length - 1; i >= 0; i--) {
+					this.particles[i].$element.data('stellar-elementIsActive', undefined);
+				}
+			}
+
+			this.particles = [];
+
+			if (!this.options.parallaxElements) return;
+
+			this.$element.find('[data-stellar-ratio]').each(function(i) {
+				var $this = $(this),
+					horizontalOffset,
+					verticalOffset,
+					positionLeft,
+					positionTop,
+					marginLeft,
+					marginTop,
+					$offsetParent,
+					offsetLeft,
+					offsetTop,
+					parentOffsetLeft = 0,
+					parentOffsetTop = 0,
+					tempParentOffsetLeft = 0,
+					tempParentOffsetTop = 0;
+
+				// Ensure this element isn't already part of another scrolling element
+				if (!$this.data('stellar-elementIsActive')) {
+					$this.data('stellar-elementIsActive', this);
+				} else if ($this.data('stellar-elementIsActive') !== this) {
+					return;
+				}
+
+				self.options.showElement($this);
+
+				// Save/restore the original top and left CSS values in case we refresh the particles or destroy the instance
+				if (!$this.data('stellar-startingLeft')) {
+					$this.data('stellar-startingLeft', $this.css('left'));
+					$this.data('stellar-startingTop', $this.css('top'));
+				} else {
+					$this.css('left', $this.data('stellar-startingLeft'));
+					$this.css('top', $this.data('stellar-startingTop'));
+				}
+
+				positionLeft = $this.position().left;
+				positionTop = $this.position().top;
+
+				// Catch-all for margin top/left properties (these evaluate to 'auto' in IE7 and IE8)
+				marginLeft = ($this.css('margin-left') === 'auto') ? 0 : parseInt($this.css('margin-left'), 10);
+				marginTop = ($this.css('margin-top') === 'auto') ? 0 : parseInt($this.css('margin-top'), 10);
+
+				offsetLeft = $this.offset().left - marginLeft;
+				offsetTop = $this.offset().top - marginTop;
+
+				// Calculate the offset parent
+				$this.parents().each(function() {
+					var $this = $(this);
+
+					if ($this.data('stellar-offset-parent') === true) {
+						parentOffsetLeft = tempParentOffsetLeft;
+						parentOffsetTop = tempParentOffsetTop;
+						$offsetParent = $this;
+
+						return false;
+					} else {
+						tempParentOffsetLeft += $this.position().left;
+						tempParentOffsetTop += $this.position().top;
+					}
+				});
+
+				// Detect the offsets
+				horizontalOffset = ($this.data('stellar-horizontal-offset') !== undefined ? $this.data('stellar-horizontal-offset') : ($offsetParent !== undefined && $offsetParent.data('stellar-horizontal-offset') !== undefined ? $offsetParent.data('stellar-horizontal-offset') : self.horizontalOffset));
+				verticalOffset = ($this.data('stellar-vertical-offset') !== undefined ? $this.data('stellar-vertical-offset') : ($offsetParent !== undefined && $offsetParent.data('stellar-vertical-offset') !== undefined ? $offsetParent.data('stellar-vertical-offset') : self.verticalOffset));
+
+				// Add our object to the particles collection
+				self.particles.push({
+					$element: $this,
+					$offsetParent: $offsetParent,
+					isFixed: $this.css('position') === 'fixed',
+					horizontalOffset: horizontalOffset,
+					verticalOffset: verticalOffset,
+					startingPositionLeft: positionLeft,
+					startingPositionTop: positionTop,
+					startingOffsetLeft: offsetLeft,
+					startingOffsetTop: offsetTop,
+					parentOffsetLeft: parentOffsetLeft,
+					parentOffsetTop: parentOffsetTop,
+					stellarRatio: ($this.data('stellar-ratio') !== undefined ? $this.data('stellar-ratio') : 1),
+					width: $this.outerWidth(true),
+					height: $this.outerHeight(true),
+					isHidden: false
+				});
+			});
+		},
+		_findBackgrounds: function() {
+			var self = this,
+				scrollLeft = this._getScrollLeft(),
+				scrollTop = this._getScrollTop(),
+				$backgroundElements;
+
+			this.backgrounds = [];
+
+			if (!this.options.parallaxBackgrounds) return;
+
+			$backgroundElements = this.$element.find('[data-stellar-background-ratio]');
+
+			if (this.$element.data('stellar-background-ratio')) {
+				$backgroundElements = $backgroundElements.add(this.$element);
+			}
+
+			$backgroundElements.each(function() {
+				var $this = $(this),
+					backgroundPosition = getBackgroundPosition($this),
+					horizontalOffset,
+					verticalOffset,
+					positionLeft,
+					positionTop,
+					marginLeft,
+					marginTop,
+					offsetLeft,
+					offsetTop,
+					$offsetParent,
+					parentOffsetLeft = 0,
+					parentOffsetTop = 0,
+					tempParentOffsetLeft = 0,
+					tempParentOffsetTop = 0;
+
+				// Ensure this element isn't already part of another scrolling element
+				if (!$this.data('stellar-backgroundIsActive')) {
+					$this.data('stellar-backgroundIsActive', this);
+				} else if ($this.data('stellar-backgroundIsActive') !== this) {
+					return;
+				}
+
+				// Save/restore the original top and left CSS values in case we destroy the instance
+				if (!$this.data('stellar-backgroundStartingLeft')) {
+					$this.data('stellar-backgroundStartingLeft', backgroundPosition[0]);
+					$this.data('stellar-backgroundStartingTop', backgroundPosition[1]);
+				} else {
+					setBackgroundPosition($this, $this.data('stellar-backgroundStartingLeft'), $this.data('stellar-backgroundStartingTop'));
+				}
+
+				// Catch-all for margin top/left properties (these evaluate to 'auto' in IE7 and IE8)
+				marginLeft = ($this.css('margin-left') === 'auto') ? 0 : parseInt($this.css('margin-left'), 10);
+				marginTop = ($this.css('margin-top') === 'auto') ? 0 : parseInt($this.css('margin-top'), 10);
+
+				offsetLeft = $this.offset().left - marginLeft - scrollLeft;
+				offsetTop = $this.offset().top - marginTop - scrollTop;
+
+				// Calculate the offset parent
+				$this.parents().each(function() {
+					var $this = $(this);
+
+					if ($this.data('stellar-offset-parent') === true) {
+						parentOffsetLeft = tempParentOffsetLeft;
+						parentOffsetTop = tempParentOffsetTop;
+						$offsetParent = $this;
+
+						return false;
+					} else {
+						tempParentOffsetLeft += $this.position().left;
+						tempParentOffsetTop += $this.position().top;
+					}
+				});
+
+				// Detect the offsets
+				horizontalOffset = ($this.data('stellar-horizontal-offset') !== undefined ? $this.data('stellar-horizontal-offset') : ($offsetParent !== undefined && $offsetParent.data('stellar-horizontal-offset') !== undefined ? $offsetParent.data('stellar-horizontal-offset') : self.horizontalOffset));
+				verticalOffset = ($this.data('stellar-vertical-offset') !== undefined ? $this.data('stellar-vertical-offset') : ($offsetParent !== undefined && $offsetParent.data('stellar-vertical-offset') !== undefined ? $offsetParent.data('stellar-vertical-offset') : self.verticalOffset));
+
+				self.backgrounds.push({
+					$element: $this,
+					$offsetParent: $offsetParent,
+					isFixed: $this.css('background-attachment') === 'fixed',
+					horizontalOffset: horizontalOffset,
+					verticalOffset: verticalOffset,
+					startingValueLeft: backgroundPosition[0],
+					startingValueTop: backgroundPosition[1],
+					startingBackgroundPositionLeft: (isNaN(parseInt(backgroundPosition[0], 10)) ? 0 : parseInt(backgroundPosition[0], 10)),
+					startingBackgroundPositionTop: (isNaN(parseInt(backgroundPosition[1], 10)) ? 0 : parseInt(backgroundPosition[1], 10)),
+					startingPositionLeft: $this.position().left,
+					startingPositionTop: $this.position().top,
+					startingOffsetLeft: offsetLeft,
+					startingOffsetTop: offsetTop,
+					parentOffsetLeft: parentOffsetLeft,
+					parentOffsetTop: parentOffsetTop,
+					stellarRatio: ($this.data('stellar-background-ratio') === undefined ? 1 : $this.data('stellar-background-ratio'))
+				});
+			});
+		},
+		_reset: function() {
+			var particle,
+				startingPositionLeft,
+				startingPositionTop,
+				background,
+				i;
+
+			for (i = this.particles.length - 1; i >= 0; i--) {
+				particle = this.particles[i];
+				startingPositionLeft = particle.$element.data('stellar-startingLeft');
+				startingPositionTop = particle.$element.data('stellar-startingTop');
+
+				this._setPosition(particle.$element, startingPositionLeft, startingPositionLeft, startingPositionTop, startingPositionTop);
+
+				this.options.showElement(particle.$element);
+
+				particle.$element.data('stellar-startingLeft', null).data('stellar-elementIsActive', null).data('stellar-backgroundIsActive', null);
+			}
+
+			for (i = this.backgrounds.length - 1; i >= 0; i--) {
+				background = this.backgrounds[i];
+
+				background.$element.data('stellar-backgroundStartingLeft', null).data('stellar-backgroundStartingTop', null);
+
+				setBackgroundPosition(background.$element, background.startingValueLeft, background.startingValueTop);
+			}
+		},
+		destroy: function() {
+			this._reset();
+
+			this.$scrollElement.off('resize.' + this.name).off('scroll.' + this.name);
+			this._animationLoop = $.noop;
+
+			$(window).off('load.' + this.name).off('resize.' + this.name);
+		},
+		_setOffsets: function() {
+			var self = this,
+				$window = $(window);
+
+			$window.off('resize.horizontal-' + this.name).off('resize.vertical-' + this.name);
+
+			if (typeof this.options.horizontalOffset === 'function') {
+				this.horizontalOffset = this.options.horizontalOffset();
+				$window.on('resize.horizontal-' + this.name, function() {
+					self.horizontalOffset = self.options.horizontalOffset();
+				});
+			} else {
+				this.horizontalOffset = this.options.horizontalOffset;
+			}
+
+			if (typeof this.options.verticalOffset === 'function') {
+				this.verticalOffset = this.options.verticalOffset();
+				$window.on('resize.vertical-' + this.name, function() {
+					self.verticalOffset = self.options.verticalOffset();
+				});
+			} else {
+				this.verticalOffset = this.options.verticalOffset;
+			}
+		},
+		_repositionElements: function() {
+			var scrollLeft = this._getScrollLeft(),
+				scrollTop = this._getScrollTop(),
+				horizontalOffset,
+				verticalOffset,
+				particle,
+				fixedRatioOffset,
+				background,
+				bgLeft,
+				bgTop,
+				isVisibleVertical = true,
+				isVisibleHorizontal = true,
+				newPositionLeft,
+				newPositionTop,
+				newOffsetLeft,
+				newOffsetTop,
+				i;
+
+			// First check that the scroll position or container size has changed
+			if (this.currentScrollLeft === scrollLeft && this.currentScrollTop === scrollTop && this.currentWidth === this.viewportWidth && this.currentHeight === this.viewportHeight) {
+				return;
+			} else {
+				this.currentScrollLeft = scrollLeft;
+				this.currentScrollTop = scrollTop;
+				this.currentWidth = this.viewportWidth;
+				this.currentHeight = this.viewportHeight;
+			}
+
+			// Reposition elements
+			for (i = this.particles.length - 1; i >= 0; i--) {
+				particle = this.particles[i];
+
+				fixedRatioOffset = (particle.isFixed ? 1 : 0);
+
+				// Calculate position, then calculate what the particle's new offset will be (for visibility check)
+				if (this.options.horizontalScrolling) {
+					newPositionLeft = (scrollLeft + particle.horizontalOffset + this.viewportOffsetLeft + particle.startingPositionLeft - particle.startingOffsetLeft + particle.parentOffsetLeft) * -(particle.stellarRatio + fixedRatioOffset - 1) + particle.startingPositionLeft;
+					newOffsetLeft = newPositionLeft - particle.startingPositionLeft + particle.startingOffsetLeft;
+				} else {
+					newPositionLeft = particle.startingPositionLeft;
+					newOffsetLeft = particle.startingOffsetLeft;
+				}
+
+				if (this.options.verticalScrolling) {
+					newPositionTop = (scrollTop + particle.verticalOffset + this.viewportOffsetTop + particle.startingPositionTop - particle.startingOffsetTop + particle.parentOffsetTop) * -(particle.stellarRatio + fixedRatioOffset - 1) + particle.startingPositionTop;
+					newOffsetTop = newPositionTop - particle.startingPositionTop + particle.startingOffsetTop;
+				} else {
+					newPositionTop = particle.startingPositionTop;
+					newOffsetTop = particle.startingOffsetTop;
+				}
+
+				// Check visibility
+				if (this.options.hideDistantElements) {
+					isVisibleHorizontal = !this.options.horizontalScrolling || newOffsetLeft + particle.width > (particle.isFixed ? 0 : scrollLeft) && newOffsetLeft < (particle.isFixed ? 0 : scrollLeft) + this.viewportWidth + this.viewportOffsetLeft;
+					isVisibleVertical = !this.options.verticalScrolling || newOffsetTop + particle.height > (particle.isFixed ? 0 : scrollTop) && newOffsetTop < (particle.isFixed ? 0 : scrollTop) + this.viewportHeight + this.viewportOffsetTop;
+				}
+
+				if (isVisibleHorizontal && isVisibleVertical) {
+					if (particle.isHidden) {
+						this.options.showElement(particle.$element);
+						particle.isHidden = false;
+					}
+
+					this._setPosition(particle.$element, newPositionLeft, particle.startingPositionLeft, newPositionTop, particle.startingPositionTop);
+				} else {
+					if (!particle.isHidden) {
+						this.options.hideElement(particle.$element);
+						particle.isHidden = true;
+					}
+				}
+			}
+
+			// Reposition backgrounds
+			for (i = this.backgrounds.length - 1; i >= 0; i--) {
+				background = this.backgrounds[i];
+
+				fixedRatioOffset = (background.isFixed ? 0 : 1);
+				bgLeft = (this.options.horizontalScrolling ? (scrollLeft + background.horizontalOffset - this.viewportOffsetLeft - background.startingOffsetLeft + background.parentOffsetLeft - background.startingBackgroundPositionLeft) * (fixedRatioOffset - background.stellarRatio) + 'px' : background.startingValueLeft);
+				bgTop = (this.options.verticalScrolling ? (scrollTop + background.verticalOffset - this.viewportOffsetTop - background.startingOffsetTop + background.parentOffsetTop - background.startingBackgroundPositionTop) * (fixedRatioOffset - background.stellarRatio) + 'px' : background.startingValueTop);
+
+				setBackgroundPosition(background.$element, bgLeft, bgTop);
+			}
+		},
+		_handleScrollEvent: function() {
+			var self = this,
+				ticking = false;
+
+			var update = function() {
+				self._repositionElements();
+				ticking = false;
+			};
+
+			var requestTick = function() {
+				if (!ticking) {
+					requestAnimFrame(update);
+					ticking = true;
+				}
+			};
+
+			this.$scrollElement.on('scroll.' + this.name, requestTick);
+			requestTick();
+		},
+		_startAnimationLoop: function() {
+			var self = this;
+
+			this._animationLoop = function() {
+				requestAnimFrame(self._animationLoop);
+				self._repositionElements();
+			};
+			this._animationLoop();
+		}
+	};
+
+	$.fn[pluginName] = function (options) {
+		var args = arguments;
+		if (options === undefined || typeof options === 'object') {
+			return this.each(function () {
+				if (!$.data(this, 'plugin_' + pluginName)) {
+					$.data(this, 'plugin_' + pluginName, new Plugin(this, options));
+				}
+			});
+		} else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') {
+			return this.each(function () {
+				var instance = $.data(this, 'plugin_' + pluginName);
+				if (instance instanceof Plugin && typeof instance[options] === 'function') {
+					instance[options].apply(instance, Array.prototype.slice.call(args, 1));
+				}
+				if (options === 'destroy') {
+					$.data(this, 'plugin_' + pluginName, null);
+				}
+			});
+		}
+	};
+
+	$[pluginName] = function(options) {
+		var $window = $(window);
+		return $window.stellar.apply($window, Array.prototype.slice.call(arguments, 0));
+	};
+
+	// Expose the scroll and position property function hashes so they can be extended
+	$[pluginName].scrollProperty = scrollProperty;
+	$[pluginName].positionProperty = positionProperty;
+
+	// Expose the plugin class so it can be modified
+	window.Stellar = Plugin;
+}(jQuery, this, document));
diff --git a/website/themes/lektor-icon/assets/static/js/jquery.stellar.min.js b/website/themes/lektor-icon/assets/static/js/jquery.stellar.min.js
new file mode 100644
index 0000000..2f61889
--- /dev/null
+++ b/website/themes/lektor-icon/assets/static/js/jquery.stellar.min.js
@@ -0,0 +1,2 @@
+/*! Stellar.js v0.6.2 | Copyright 2014, Mark Dalgleish | https://markdalgleish.com/projects/stellar.js | https://markdalgleish.mit-license.org */
+!function(t,e,i,s){var o="stellar",n={scrollProperty:"scroll",positionProperty:"position",horizontalScrolling:!0,verticalScrolling:!0,horizontalOffset:0,verticalOffset:0,responsive:!1,parallaxBackgrounds:!0,parallaxElements:!0,hideDistantElements:!0,hideElement:function(t){t.hide()},showElement:function(t){t.show()}},r={scroll:{getLeft:function(t){return t.scrollLeft()},setLeft:function(t,e){t.scrollLeft(e)},getTop:function(t){return t.scrollTop()},setTop:function(t,e){t.scrollTop(e)}},position:{getLeft:function(t){return-1*parseInt(t.css("left"),10)},getTop:function(t){return-1*parseInt(t.css("top"),10)}},margin:{getLeft:function(t){return-1*parseInt(t.css("margin-left"),10)},getTop:function(t){return-1*parseInt(t.css("margin-top"),10)}},transform:{getLeft:function(t){var e=getComputedStyle(t[0])[l];return"none"!==e?-1*parseInt(e.match(/(-?[0-9]+)/g)[4],10):0},getTop:function(t){var e=getComputedStyle(t[0])[l];return"none"!==e?-1*parseInt(e.match(/(-?[0-9]+)/g)[5],10):0}}},a={position:{setLeft:function(t,e){t.css("left",e)},setTop:function(t,e){t.css("top",e)}},transform:{setPosition:function(t,e,i,s,o){t[0].style[l]="translate3d("+(e-i)+"px, "+(s-o)+"px, 0)"}}},l=function(){var e,i=/^(Moz|Webkit|Khtml|O|ms|Icab)(?=[A-Z])/,s=t("script")[0].style,o="";for(e in s)if(i.test(e)){o=e.match(i)[0];break}return"WebkitOpacity"in s&&(o="Webkit"),"KhtmlOpacity"in s&&(o="Khtml"),function(t){return o+(o.length>0?t.charAt(0).toUpperCase()+t.slice(1):t)}}()("transform"),f=t("<div />",{style:"background:#fff"}).css("background-position-x")!==s,c=f?function(t,e,i){t.css({"background-position-x":e,"background-position-y":i})}:function(t,e,i){t.css("background-position",e+" "+i)},p=f?function(t){return[t.css("background-position-x"),t.css("background-position-y")]}:function(t){return t.css("background-position").split(" ")},h=e.requestAnimationFrame||e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t){setTimeout(t,1e3/60)};function d(e,i){this.element=e,this.options=t.extend({},n,i),this._defaults=n,this._name=o,this.init()}d.prototype={init:function(){this.options.name=o+"_"+Math.floor(1e9*Math.random()),this._defineElements(),this._defineGetters(),this._defineSetters(),this._handleWindowLoadAndResize(),this._detectViewport(),this.refresh({firstLoad:!0}),"scroll"===this.options.scrollProperty?this._handleScrollEvent():this._startAnimationLoop()},_defineElements:function(){this.element===i.body&&(this.element=e),this.$scrollElement=t(this.element),this.$element=this.element===e?t("body"):this.$scrollElement,this.$viewportElement=this.options.viewportElement!==s?t(this.options.viewportElement):this.$scrollElement[0]===e||"scroll"===this.options.scrollProperty?this.$scrollElement:this.$scrollElement.parent()},_defineGetters:function(){var t=this,e=r[t.options.scrollProperty];this._getScrollLeft=function(){return e.getLeft(t.$scrollElement)},this._getScrollTop=function(){return e.getTop(t.$scrollElement)}},_defineSetters:function(){var e=this,i=r[e.options.scrollProperty],s=a[e.options.positionProperty],o=i.setLeft,n=i.setTop;this._setScrollLeft="function"==typeof o?function(t){o(e.$scrollElement,t)}:t.noop,this._setScrollTop="function"==typeof n?function(t){n(e.$scrollElement,t)}:t.noop,this._setPosition=s.setPosition||function(t,i,o,n,r){e.options.horizontalScrolling&&s.setLeft(t,i,o),e.options.verticalScrolling&&s.setTop(t,n,r)}},_handleWindowLoadAndResize:function(){var i=this,s=t(e);i.options.responsive&&s.on("load."+this.name,function(){i.refresh()}),s.on("resize."+this.name,function(){i._detectViewport(),i.options.responsive&&i.refresh()})},refresh:function(i){var s=this,o=s._getScrollLeft(),n=s._getScrollTop();i&&i.firstLoad||this._reset(),this._setScrollLeft(0),this._setScrollTop(0),this._setOffsets(),this._findParticles(),this._findBackgrounds(),i&&i.firstLoad&&/WebKit/.test(navigator.userAgent)&&t(e).on("load",function(){var t=s._getScrollLeft(),e=s._getScrollTop();s._setScrollLeft(t+1),s._setScrollTop(e+1),s._setScrollLeft(t),s._setScrollTop(e)}),this._setScrollLeft(o),this._setScrollTop(n)},_detectViewport:function(){var t=this.$viewportElement[0]!==e?this.$viewportElement.offset():{top:0,left:0},i=null!==t&&t!==s;this.viewportWidth=this.$viewportElement.width(),this.viewportHeight=this.$viewportElement.height(),this.viewportOffsetTop=i?t.top:0,this.viewportOffsetLeft=i?t.left:0},_findParticles:function(){var e=this;this._getScrollLeft(),this._getScrollTop();if(this.particles!==s)for(var i=this.particles.length-1;i>=0;i--)this.particles[i].$element.data("stellar-elementIsActive",s);this.particles=[],this.options.parallaxElements&&this.$element.find("[data-stellar-ratio]").each(function(i){var o,n,r,a,l,f,c,p,h,d=t(this),u=0,g=0,m=0,v=0;if(d.data("stellar-elementIsActive")){if(d.data("stellar-elementIsActive")!==this)return}else d.data("stellar-elementIsActive",this);e.options.showElement(d),d.data("stellar-startingLeft")?(d.css("left",d.data("stellar-startingLeft")),d.css("top",d.data("stellar-startingTop"))):(d.data("stellar-startingLeft",d.css("left")),d.data("stellar-startingTop",d.css("top"))),r=d.position().left,a=d.position().top,l="auto"===d.css("margin-left")?0:parseInt(d.css("margin-left"),10),f="auto"===d.css("margin-top")?0:parseInt(d.css("margin-top"),10),p=d.offset().left-l,h=d.offset().top-f,d.parents().each(function(){var e=t(this);if(!0===e.data("stellar-offset-parent"))return u=m,g=v,c=e,!1;m+=e.position().left,v+=e.position().top}),o=d.data("stellar-horizontal-offset")!==s?d.data("stellar-horizontal-offset"):c!==s&&c.data("stellar-horizontal-offset")!==s?c.data("stellar-horizontal-offset"):e.horizontalOffset,n=d.data("stellar-vertical-offset")!==s?d.data("stellar-vertical-offset"):c!==s&&c.data("stellar-vertical-offset")!==s?c.data("stellar-vertical-offset"):e.verticalOffset,e.particles.push({$element:d,$offsetParent:c,isFixed:"fixed"===d.css("position"),horizontalOffset:o,verticalOffset:n,startingPositionLeft:r,startingPositionTop:a,startingOffsetLeft:p,startingOffsetTop:h,parentOffsetLeft:u,parentOffsetTop:g,stellarRatio:d.data("stellar-ratio")!==s?d.data("stellar-ratio"):1,width:d.outerWidth(!0),height:d.outerHeight(!0),isHidden:!1})})},_findBackgrounds:function(){var e,i=this,o=this._getScrollLeft(),n=this._getScrollTop();this.backgrounds=[],this.options.parallaxBackgrounds&&(e=this.$element.find("[data-stellar-background-ratio]"),this.$element.data("stellar-background-ratio")&&(e=e.add(this.$element)),e.each(function(){var e,r,a,l,f,h,d,u=t(this),g=p(u),m=0,v=0,L=0,_=0;if(u.data("stellar-backgroundIsActive")){if(u.data("stellar-backgroundIsActive")!==this)return}else u.data("stellar-backgroundIsActive",this);u.data("stellar-backgroundStartingLeft")?c(u,u.data("stellar-backgroundStartingLeft"),u.data("stellar-backgroundStartingTop")):(u.data("stellar-backgroundStartingLeft",g[0]),u.data("stellar-backgroundStartingTop",g[1])),a="auto"===u.css("margin-left")?0:parseInt(u.css("margin-left"),10),l="auto"===u.css("margin-top")?0:parseInt(u.css("margin-top"),10),f=u.offset().left-a-o,h=u.offset().top-l-n,u.parents().each(function(){var e=t(this);if(!0===e.data("stellar-offset-parent"))return m=L,v=_,d=e,!1;L+=e.position().left,_+=e.position().top}),e=u.data("stellar-horizontal-offset")!==s?u.data("stellar-horizontal-offset"):d!==s&&d.data("stellar-horizontal-offset")!==s?d.data("stellar-horizontal-offset"):i.horizontalOffset,r=u.data("stellar-vertical-offset")!==s?u.data("stellar-vertical-offset"):d!==s&&d.data("stellar-vertical-offset")!==s?d.data("stellar-vertical-offset"):i.verticalOffset,i.backgrounds.push({$element:u,$offsetParent:d,isFixed:"fixed"===u.css("background-attachment"),horizontalOffset:e,verticalOffset:r,startingValueLeft:g[0],startingValueTop:g[1],startingBackgroundPositionLeft:isNaN(parseInt(g[0],10))?0:parseInt(g[0],10),startingBackgroundPositionTop:isNaN(parseInt(g[1],10))?0:parseInt(g[1],10),startingPositionLeft:u.position().left,startingPositionTop:u.position().top,startingOffsetLeft:f,startingOffsetTop:h,parentOffsetLeft:m,parentOffsetTop:v,stellarRatio:u.data("stellar-background-ratio")===s?1:u.data("stellar-background-ratio")})}))},_reset:function(){var t,e,i,s,o;for(o=this.particles.length-1;o>=0;o--)e=(t=this.particles[o]).$element.data("stellar-startingLeft"),i=t.$element.data("stellar-startingTop"),this._setPosition(t.$element,e,e,i,i),this.options.showElement(t.$element),t.$element.data("stellar-startingLeft",null).data("stellar-elementIsActive",null).data("stellar-backgroundIsActive",null);for(o=this.backgrounds.length-1;o>=0;o--)(s=this.backgrounds[o]).$element.data("stellar-backgroundStartingLeft",null).data("stellar-backgroundStartingTop",null),c(s.$element,s.startingValueLeft,s.startingValueTop)},destroy:function(){this._reset(),this.$scrollElement.off("resize."+this.name).off("scroll."+this.name),this._animationLoop=t.noop,t(e).off("load."+this.name).off("resize."+this.name)},_setOffsets:function(){var i=this,s=t(e);s.off("resize.horizontal-"+this.name).off("resize.vertical-"+this.name),"function"==typeof this.options.horizontalOffset?(this.horizontalOffset=this.options.horizontalOffset(),s.on("resize.horizontal-"+this.name,function(){i.horizontalOffset=i.options.horizontalOffset()})):this.horizontalOffset=this.options.horizontalOffset,"function"==typeof this.options.verticalOffset?(this.verticalOffset=this.options.verticalOffset(),s.on("resize.vertical-"+this.name,function(){i.verticalOffset=i.options.verticalOffset()})):this.verticalOffset=this.options.verticalOffset},_repositionElements:function(){var t,e,i,s,o,n,r,a,l,f,p=this._getScrollLeft(),h=this._getScrollTop(),d=!0,u=!0;if(this.currentScrollLeft!==p||this.currentScrollTop!==h||this.currentWidth!==this.viewportWidth||this.currentHeight!==this.viewportHeight){for(this.currentScrollLeft=p,this.currentScrollTop=h,this.currentWidth=this.viewportWidth,this.currentHeight=this.viewportHeight,f=this.particles.length-1;f>=0;f--)e=(t=this.particles[f]).isFixed?1:0,this.options.horizontalScrolling?a=(n=(p+t.horizontalOffset+this.viewportOffsetLeft+t.startingPositionLeft-t.startingOffsetLeft+t.parentOffsetLeft)*-(t.stellarRatio+e-1)+t.startingPositionLeft)-t.startingPositionLeft+t.startingOffsetLeft:(n=t.startingPositionLeft,a=t.startingOffsetLeft),this.options.verticalScrolling?l=(r=(h+t.verticalOffset+this.viewportOffsetTop+t.startingPositionTop-t.startingOffsetTop+t.parentOffsetTop)*-(t.stellarRatio+e-1)+t.startingPositionTop)-t.startingPositionTop+t.startingOffsetTop:(r=t.startingPositionTop,l=t.startingOffsetTop),this.options.hideDistantElements&&(u=!this.options.horizontalScrolling||a+t.width>(t.isFixed?0:p)&&a<(t.isFixed?0:p)+this.viewportWidth+this.viewportOffsetLeft,d=!this.options.verticalScrolling||l+t.height>(t.isFixed?0:h)&&l<(t.isFixed?0:h)+this.viewportHeight+this.viewportOffsetTop),u&&d?(t.isHidden&&(this.options.showElement(t.$element),t.isHidden=!1),this._setPosition(t.$element,n,t.startingPositionLeft,r,t.startingPositionTop)):t.isHidden||(this.options.hideElement(t.$element),t.isHidden=!0);for(f=this.backgrounds.length-1;f>=0;f--)e=(i=this.backgrounds[f]).isFixed?0:1,s=this.options.horizontalScrolling?(p+i.horizontalOffset-this.viewportOffsetLeft-i.startingOffsetLeft+i.parentOffsetLeft-i.startingBackgroundPositionLeft)*(e-i.stellarRatio)+"px":i.startingValueLeft,o=this.options.verticalScrolling?(h+i.verticalOffset-this.viewportOffsetTop-i.startingOffsetTop+i.parentOffsetTop-i.startingBackgroundPositionTop)*(e-i.stellarRatio)+"px":i.startingValueTop,c(i.$element,s,o)}},_handleScrollEvent:function(){var t=this,e=!1,i=function(){t._repositionElements(),e=!1},s=function(){e||(h(i),e=!0)};this.$scrollElement.on("scroll."+this.name,s),s()},_startAnimationLoop:function(){var t=this;this._animationLoop=function(){h(t._animationLoop),t._repositionElements()},this._animationLoop()}},t.fn[o]=function(e){var i=arguments;return e===s||"object"==typeof e?this.each(function(){t.data(this,"plugin_"+o)||t.data(this,"plugin_"+o,new d(this,e))}):"string"==typeof e&&"_"!==e[0]&&"init"!==e?this.each(function(){var s=t.data(this,"plugin_"+o);s instanceof d&&"function"==typeof s[e]&&s[e].apply(s,Array.prototype.slice.call(i,1)),"destroy"===e&&t.data(this,"plugin_"+o,null)}):void 0},t[o]=function(i){var s=t(e);return s.stellar.apply(s,Array.prototype.slice.call(arguments,0))},t[o].scrollProperty=r,t[o].positionProperty=a,e.Stellar=d}(jQuery,this,document);
diff --git a/website/themes/lektor-icon/assets/static/js/jquery.waypoints.min.js b/website/themes/lektor-icon/assets/static/js/jquery.waypoints.min.js
new file mode 100644
index 0000000..7f7e8f3
--- /dev/null
+++ b/website/themes/lektor-icon/assets/static/js/jquery.waypoints.min.js
@@ -0,0 +1,7 @@
+/*!
+Waypoints - 4.0.1
+Copyright © 2011-2016 Caleb Troughton
+Licensed under the MIT license.
+https://github.com/imakewebthings/waypoints/blob/master/licenses.txt
+*/
+!function(){"use strict";function t(o){if(!o)throw new Error("No options passed to Waypoint constructor");if(!o.element)throw new Error("No element option passed to Waypoint constructor");if(!o.handler)throw new Error("No handler option passed to Waypoint constructor");this.key="waypoint-"+e,this.options=t.Adapter.extend({},t.defaults,o),this.element=this.options.element,this.adapter=new t.Adapter(this.element),this.callback=o.handler,this.axis=this.options.horizontal?"horizontal":"vertical",this.enabled=this.options.enabled,this.triggerPoint=null,this.group=t.Group.findOrCreate({name:this.options.group,axis:this.axis}),this.context=t.Context.findOrCreateByElement(this.options.context),t.offsetAliases[this.options.offset]&&(this.options.offset=t.offsetAliases[this.options.offset]),this.group.add(this),this.context.add(this),i[this.key]=this,e+=1}var e=0,i={};t.prototype.queueTrigger=function(t){this.group.queueTrigger(this,t)},t.prototype.trigger=function(t){this.enabled&&this.callback&&this.callback.apply(this,t)},t.prototype.destroy=function(){this.context.remove(this),this.group.remove(this),delete i[this.key]},t.prototype.disable=function(){return this.enabled=!1,this},t.prototype.enable=function(){return this.context.refresh(),this.enabled=!0,this},t.prototype.next=function(){return this.group.next(this)},t.prototype.previous=function(){return this.group.previous(this)},t.invokeAll=function(t){var e=[];for(var o in i)e.push(i[o]);for(var n=0,r=e.length;r>n;n++)e[n][t]()},t.destroyAll=function(){t.invokeAll("destroy")},t.disableAll=function(){t.invokeAll("disable")},t.enableAll=function(){t.Context.refreshAll();for(var e in i)i[e].enabled=!0;return this},t.refreshAll=function(){t.Context.refreshAll()},t.viewportHeight=function(){return window.innerHeight||document.documentElement.clientHeight},t.viewportWidth=function(){return document.documentElement.clientWidth},t.adapters=[],t.defaults={context:window,continuous:!0,enabled:!0,group:"default",horizontal:!1,offset:0},t.offsetAliases={"bottom-in-view":function(){return this.context.innerHeight()-this.adapter.outerHeight()},"right-in-view":function(){return this.context.innerWidth()-this.adapter.outerWidth()}},window.Waypoint=t}(),function(){"use strict";function t(t){window.setTimeout(t,1e3/60)}function e(t){this.element=t,this.Adapter=n.Adapter,this.adapter=new this.Adapter(t),this.key="waypoint-context-"+i,this.didScroll=!1,this.didResize=!1,this.oldScroll={x:this.adapter.scrollLeft(),y:this.adapter.scrollTop()},this.waypoints={vertical:{},horizontal:{}},t.waypointContextKey=this.key,o[t.waypointContextKey]=this,i+=1,n.windowContext||(n.windowContext=!0,n.windowContext=new e(window)),this.createThrottledScrollHandler(),this.createThrottledResizeHandler()}var i=0,o={},n=window.Waypoint,r=window.onload;e.prototype.add=function(t){var e=t.options.horizontal?"horizontal":"vertical";this.waypoints[e][t.key]=t,this.refresh()},e.prototype.checkEmpty=function(){var t=this.Adapter.isEmptyObject(this.waypoints.horizontal),e=this.Adapter.isEmptyObject(this.waypoints.vertical),i=this.element==this.element.window;t&&e&&!i&&(this.adapter.off(".waypoints"),delete o[this.key])},e.prototype.createThrottledResizeHandler=function(){function t(){e.handleResize(),e.didResize=!1}var e=this;this.adapter.on("resize.waypoints",function(){e.didResize||(e.didResize=!0,n.requestAnimationFrame(t))})},e.prototype.createThrottledScrollHandler=function(){function t(){e.handleScroll(),e.didScroll=!1}var e=this;this.adapter.on("scroll.waypoints",function(){(!e.didScroll||n.isTouch)&&(e.didScroll=!0,n.requestAnimationFrame(t))})},e.prototype.handleResize=function(){n.Context.refreshAll()},e.prototype.handleScroll=function(){var t={},e={horizontal:{newScroll:this.adapter.scrollLeft(),oldScroll:this.oldScroll.x,forward:"right",backward:"left"},vertical:{newScroll:this.adapter.scrollTop(),oldScroll:this.oldScroll.y,forward:"down",backward:"up"}};for(var i in e){var o=e[i],n=o.newScroll>o.oldScroll,r=n?o.forward:o.backward;for(var s in this.waypoints[i]){var a=this.waypoints[i][s];if(null!==a.triggerPoint){var l=o.oldScroll<a.triggerPoint,h=o.newScroll>=a.triggerPoint,p=l&&h,u=!l&&!h;(p||u)&&(a.queueTrigger(r),t[a.group.id]=a.group)}}}for(var c in t)t[c].flushTriggers();this.oldScroll={x:e.horizontal.newScroll,y:e.vertical.newScroll}},e.prototype.innerHeight=function(){return this.element==this.element.window?n.viewportHeight():this.adapter.innerHeight()},e.prototype.remove=function(t){delete this.waypoints[t.axis][t.key],this.checkEmpty()},e.prototype.innerWidth=function(){return this.element==this.element.window?n.viewportWidth():this.adapter.innerWidth()},e.prototype.destroy=function(){var t=[];for(var e in this.waypoints)for(var i in this.waypoints[e])t.push(this.waypoints[e][i]);for(var o=0,n=t.length;n>o;o++)t[o].destroy()},e.prototype.refresh=function(){var t,e=this.element==this.element.window,i=e?void 0:this.adapter.offset(),o={};this.handleScroll(),t={horizontal:{contextOffset:e?0:i.left,contextScroll:e?0:this.oldScroll.x,contextDimension:this.innerWidth(),oldScroll:this.oldScroll.x,forward:"right",backward:"left",offsetProp:"left"},vertical:{contextOffset:e?0:i.top,contextScroll:e?0:this.oldScroll.y,contextDimension:this.innerHeight(),oldScroll:this.oldScroll.y,forward:"down",backward:"up",offsetProp:"top"}};for(var r in t){var s=t[r];for(var a in this.waypoints[r]){var l,h,p,u,c,d=this.waypoints[r][a],f=d.options.offset,w=d.triggerPoint,y=0,g=null==w;d.element!==d.element.window&&(y=d.adapter.offset()[s.offsetProp]),"function"==typeof f?f=f.apply(d):"string"==typeof f&&(f=parseFloat(f),d.options.offset.indexOf("%")>-1&&(f=Math.ceil(s.contextDimension*f/100))),l=s.contextScroll-s.contextOffset,d.triggerPoint=Math.floor(y+l-f),h=w<s.oldScroll,p=d.triggerPoint>=s.oldScroll,u=h&&p,c=!h&&!p,!g&&u?(d.queueTrigger(s.backward),o[d.group.id]=d.group):!g&&c?(d.queueTrigger(s.forward),o[d.group.id]=d.group):g&&s.oldScroll>=d.triggerPoint&&(d.queueTrigger(s.forward),o[d.group.id]=d.group)}}return n.requestAnimationFrame(function(){for(var t in o)o[t].flushTriggers()}),this},e.findOrCreateByElement=function(t){return e.findByElement(t)||new e(t)},e.refreshAll=function(){for(var t in o)o[t].refresh()},e.findByElement=function(t){return o[t.waypointContextKey]},window.onload=function(){r&&r(),e.refreshAll()},n.requestAnimationFrame=function(e){var i=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||t;i.call(window,e)},n.Context=e}(),function(){"use strict";function t(t,e){return t.triggerPoint-e.triggerPoint}function e(t,e){return e.triggerPoint-t.triggerPoint}function i(t){this.name=t.name,this.axis=t.axis,this.id=this.name+"-"+this.axis,this.waypoints=[],this.clearTriggerQueues(),o[this.axis][this.name]=this}var o={vertical:{},horizontal:{}},n=window.Waypoint;i.prototype.add=function(t){this.waypoints.push(t)},i.prototype.clearTriggerQueues=function(){this.triggerQueues={up:[],down:[],left:[],right:[]}},i.prototype.flushTriggers=function(){for(var i in this.triggerQueues){var o=this.triggerQueues[i],n="up"===i||"left"===i;o.sort(n?e:t);for(var r=0,s=o.length;s>r;r+=1){var a=o[r];(a.options.continuous||r===o.length-1)&&a.trigger([i])}}this.clearTriggerQueues()},i.prototype.next=function(e){this.waypoints.sort(t);var i=n.Adapter.inArray(e,this.waypoints),o=i===this.waypoints.length-1;return o?null:this.waypoints[i+1]},i.prototype.previous=function(e){this.waypoints.sort(t);var i=n.Adapter.inArray(e,this.waypoints);return i?this.waypoints[i-1]:null},i.prototype.queueTrigger=function(t,e){this.triggerQueues[e].push(t)},i.prototype.remove=function(t){var e=n.Adapter.inArray(t,this.waypoints);e>-1&&this.waypoints.splice(e,1)},i.prototype.first=function(){return this.waypoints[0]},i.prototype.last=function(){return this.waypoints[this.waypoints.length-1]},i.findOrCreate=function(t){return o[t.axis][t.name]||new i(t)},n.Group=i}(),function(){"use strict";function t(t){this.$element=e(t)}var e=window.jQuery,i=window.Waypoint;e.each(["innerHeight","innerWidth","off","offset","on","outerHeight","outerWidth","scrollLeft","scrollTop"],function(e,i){t.prototype[i]=function(){var t=Array.prototype.slice.call(arguments);return this.$element[i].apply(this.$element,t)}}),e.each(["extend","inArray","isEmptyObject"],function(i,o){t[o]=e[o]}),i.adapters.push({name:"jquery",Adapter:t}),i.Adapter=t}(),function(){"use strict";function t(t){return function(){var i=[],o=arguments[0];return t.isFunction(arguments[0])&&(o=t.extend({},arguments[1]),o.handler=arguments[0]),this.each(function(){var n=t.extend({},o,{element:this});"string"==typeof n.context&&(n.context=t(this).closest(n.context)[0]),i.push(new e(n))}),i}}var e=window.Waypoint;window.jQuery&&(window.jQuery.fn.waypoint=t(window.jQuery)),window.Zepto&&(window.Zepto.fn.waypoint=t(window.Zepto))}();
diff --git a/website/themes/lektor-icon/assets/static/js/magnific-popup-options.js b/website/themes/lektor-icon/assets/static/js/magnific-popup-options.js
new file mode 100644
index 0000000..fafcf65
--- /dev/null
+++ b/website/themes/lektor-icon/assets/static/js/magnific-popup-options.js
@@ -0,0 +1,28 @@
+$(document).ready(function() {
+   // MagnificPopup
+    var magnifPopup = function() {
+        $('.image-popup').magnificPopup({
+            type: 'image',
+            removalDelay: 300,
+            mainClass: 'mfp-with-zoom',
+            gallery:{
+                enabled:true
+            }
+        });
+    };
+
+    var magnifVideo = function() {
+        $('.popup-youtube, .popup-vimeo, .popup-gmaps').magnificPopup({
+        disableOn: 700,
+        type: 'iframe',
+        mainClass: 'mfp-fade',
+        removalDelay: 160,
+        preloader: false,
+        fixedContentPos: false
+    });
+    };
+
+    // Call the functions
+    magnifPopup();
+    magnifVideo();
+});
diff --git a/website/themes/lektor-icon/assets/static/js/main-singlelayout.js b/website/themes/lektor-icon/assets/static/js/main-singlelayout.js
new file mode 100644
index 0000000..51eae56
--- /dev/null
+++ b/website/themes/lektor-icon/assets/static/js/main-singlelayout.js
@@ -0,0 +1,229 @@
+/*
+Lektor-Icon Theme
+Copyright (c) 2016- Lektor-Icon Contributors
+
+Original standalone HTML5 theme distributed under the terms of the
+Creative Commons Attribution 3.0 license -->
+https://creativecommons.org/licenses/by/3.0/
+
+Additions, modifications and porting released under the terms of the
+MIT (Expat) License: https://opensource.org/licenses/MIT
+See the LICENSE.txt file for more details
+https://github.com/spyder-ide/lektor-icon/blob/master/LICENSE.txt
+
+For information on the included third-party assets, see NOTICE.txt
+https://github.com/spyder-ide/lektor-icon/blob/master/NOTICE.txt
+*/
+
+
+;(function () {
+
+    'use strict';
+
+
+    var isMSIE = function() {
+        var is_ie = /MSIE|Trident/.test(window.navigator.userAgent);
+        return is_ie;
+    }
+
+
+    var heroHeight = function() {
+        if ($(window).width() >= 752) {
+            $('.js-fullheight-home').css('height', $(window).height() - $('.js-sticky').height());
+        } else {
+            $('.js-fullheight-home').css('height', $(window).height() / 2);
+        }
+    };
+
+    var setHeroHeight = function() {
+        heroHeight();
+        $(window).on('resize', heroHeight);
+    };
+
+
+    // Loading animation
+    var loaderPage = function() {
+        $(".fh5co-loader").fadeOut("slow");
+    };
+
+
+    // Show and hide tab content and images on click in the mission section
+    var fh5coTabs = function() {
+        $('.fh5co-tabs li a').on('click', function(event) {
+            event.preventDefault();
+            var $this = $(this),
+                tab = $this.data('tab');
+            $('.fh5co-tabs li').removeClass('active');
+            $this.closest('li').addClass('active');
+            $this.closest('.fh5co-tabs-container').find('.fh5co-tab-content').removeClass('active');
+            $this.closest('.fh5co-tabs-container').find('.fh5co-tab-content[data-tab-content="' + tab + '"]').addClass('active');
+            $this.closest('.body-section').find('.tab-image').removeClass('active');
+            $this.closest('.body-section').find('.tab-image[data-tab-content="' + tab + '"]').addClass('active');
+        });
+    }
+
+
+    var gridAutoHeight = function() {
+        $('.fh5co-grid-item').css('height', $('.fh5co-2col-inner').outerHeight()/2);
+
+        $(window).on('resize', function(){
+            $('.fh5co-grid-item').css('height', $('.fh5co-2col-inner').outerHeight()/2);
+        });
+    }
+
+
+    // Equalize heights of cards in team/services section for proper layout
+    var cardsEvenHeight = function() {
+        $('.body-section .container').each(function() {
+            if ($(this).find('.card-inner').length && $(this).find('.card-outer').first().width() <= $(this).width() / 2.0) {
+                var cardHeightMax = 0;
+                $(this).find('.card-inner').each(function() {
+                    var cardHeight = $(this).height()
+                    if (cardHeight > cardHeightMax) {cardHeightMax = cardHeight; }
+                });
+            }
+            $(this).find('.card').each(function() {
+                var cardHeight = $(this).find('.card-inner').height()
+                $(this).find('.card-spacer').height(cardHeightMax - cardHeight);
+            });
+        });
+    }
+
+
+    var setCardsEvenHeight = function() {
+        $(window).on('load', cardsEvenHeight);
+        $(window).on('resize', cardsEvenHeight);
+        // For IE11, which doesn't work with load
+        window.setTimeout(cardsEvenHeight, 2000);
+        window.setTimeout(cardsEvenHeight, 5000);
+    };
+
+
+    // Parallax
+    var parallax = function() {
+
+        var vertical_offset_const = !isMSIE() * 51
+        $(window).stellar({horizontalScrolling: false, verticalOffset: (vertical_offset_const + $('.fh5co-main-nav').height()), responsive: false});
+    };
+
+
+    // Hide the sidebar if user scrolls the page
+    var scrolledWindow = function() {
+
+        $(window).on('scroll', function(){
+
+            var scrollPos = $(this).scrollTop();
+
+
+           if ( $('body').hasClass('offcanvas-visible') ) {
+            $('body').removeClass('offcanvas-visible');
+            $('.js-fh5co-nav-toggle').removeClass('active');
+           }
+        });
+
+        $(window).on('resize', function() {
+            if ( $('body').hasClass('offcanvas-visible') ) {
+            $('body').removeClass('offcanvas-visible');
+            $('.js-fh5co-nav-toggle').removeClass('active');
+           }
+        });
+    };
+
+
+    // Just like it says on the tin
+    var goToTop = function() {
+
+        $('.js-gotop').on('click', function(event){
+
+            event.preventDefault();
+
+            $('html, body').animate({
+                scrollTop: $('html').offset().top
+            }, 500, 'easeInOutExpo');
+
+            return false;
+        });
+
+        $(window).on('scroll', function(){
+
+            var $win = $(window);
+            if ($win.scrollTop() > 200) {
+                $('.js-top').addClass('active');
+            } else {
+                $('.js-top').removeClass('active');
+            }
+        });
+    };
+
+
+    // Page Nav
+    var clickMenu = function() {
+        var topVal = ( $(window).width() < 769 ) ? 0 : 58;
+
+        $(window).on('resize', function(){
+            topVal = ( $(window).width() < 769 ) ? 0 : 58;
+        });
+        $('.fh5co-main-nav a:not([class="external"]), #fh5co-offcanvas a:not([class="external"]), a.fh5co-content-nav:not([class="external"])').on('click', function(event){
+            var section = $(this).data('nav-section');
+
+                if ( $('div[data-section="' + section + '"]').length ) {
+
+                    $('html, body').animate({
+                        scrollTop: $('div[data-section="' + section + '"]').offset().top - topVal
+                    }, 500, 'easeInOutExpo');
+
+                    event.preventDefault();
+               }
+        });
+    };
+
+
+    // Reflect scrolling in navigation
+    var navActive = function(section) {
+
+        $('.fh5co-main-nav a[data-nav-section], #fh5co-offcanvas a[data-nav-section]').removeClass('active');
+        $('.fh5co-main-nav, #fh5co-offcanvas').find('a[data-nav-section="'+section+'"]').addClass('active');
+    };
+
+
+    // A section to scroll to on the mainpage
+    var navigationSection = function() {
+
+        var $section = $('div[data-section]');
+
+        $section.waypoint(function(direction) {
+            if (direction === 'down') {
+                navActive($(this.element).data('section'));
+            }
+
+        }, {
+            offset: '150px'
+        });
+
+        $section.waypoint(function(direction) {
+            if (direction === 'up') {
+                navActive($(this.element).data('section'));
+            }
+        }, {
+            offset: function() { return -$(this.element).height() + 155; }
+        });
+    };
+
+
+
+    // Document on load
+    $(function() {
+        setHeroHeight();
+        loaderPage();
+        fh5coTabs();
+        // gridAutoHeight();
+
+        parallax();
+        scrolledWindow();
+        goToTop();
+        clickMenu();
+        navigationSection();
+        setCardsEvenHeight();
+    });
+
+}());
diff --git a/website/themes/lektor-icon/assets/static/js/main.js b/website/themes/lektor-icon/assets/static/js/main.js
new file mode 100644
index 0000000..16765e5
--- /dev/null
+++ b/website/themes/lektor-icon/assets/static/js/main.js
@@ -0,0 +1,124 @@
+/*
+Lektor-Icon Theme
+Copyright (c) 2016- Lektor-Icon Contributors
+
+Original standalone HTML5 theme distributed under the terms of the
+Creative Commons Attribution 3.0 license -->
+https://creativecommons.org/licenses/by/3.0/
+
+Additions, modifications and porting released under the terms of the
+MIT (Expat) License: https://opensource.org/licenses/MIT
+See the LICENSE.txt file for more details
+https://github.com/spyder-ide/lektor-icon/blob/master/LICENSE.txt
+
+For information on the included third-party assets, see NOTICE.txt
+https://github.com/spyder-ide/lektor-icon/blob/master/NOTICE.txt
+*/
+
+
+;(function () {
+
+    'use strict';
+
+
+    // Fullsize Error Page Background
+    var fullHeight = function() {
+        $('#error-page').css('height', $(window).height() - $('.js-sticky').height() - $('#fh5co-footer').outerHeight());
+    };
+
+    var setFullHeight = function() {
+        fullHeight();
+        $(window).on('resize', fullHeight);
+    };
+
+
+    // Offcanvas layout for "hamburger" mobile menu
+    var offcanvasMenu = function() {
+        $('body').prepend('<div id="fh5co-offcanvas"></div>');
+        $('body').prepend('<a href="#" class="js-fh5co-nav-toggle fh5co-nav-toggle" aria-label="Toggle for hamburger menu"><i></i></a>');
+
+        $('.fh5co-main-nav .fh5co-menu-1 a, .fh5co-main-nav .fh5co-menu-2 a').each(function(){
+
+            var $this = $(this);
+
+            $('#fh5co-offcanvas').append($this.clone());
+        });
+        // $('#fh5co-offcanvas').append
+    };
+
+
+    // Top navbar stickiness
+    var mainMenuSticky = function() {
+
+        var sticky = $('.js-sticky');
+        var $section = $('.fh5co-main-nav');
+
+        sticky.css('height', sticky.height());
+        $(window).on('resize', function(){
+            sticky.css('height',  $section.height());
+        });
+
+        $section.waypoint(function(direction) {
+
+            if (direction === 'down') {
+
+                    $section.css({
+                        'position' : 'fixed',
+                        'top' : 0,
+                        'width' : '100%',
+                        'z-index' : 99999
+                    }).addClass('fh5co-shadow');;
+            }
+        }, {
+            offset: '0px'
+        });
+    };
+
+
+    // Mobile "burger" menu
+    var burgerMenu = function() {
+
+        $('body').on('click', '.js-fh5co-nav-toggle', function(event){
+
+            var $this = $(this);
+
+            if( $('body').hasClass('offcanvas-visible') ) {
+                $('body').removeClass('offcanvas-visible fh5co-overflow');
+                $this.removeClass('active');
+            } else {
+                $('body').addClass('offcanvas-visible fh5co-overflow');
+                $this.addClass('active');
+            }
+            event.preventDefault();
+        });
+    };
+
+
+    // Click outside of offcanvas sidebar to close it
+    var mobileMenuOutsideClick = function() {
+
+        $(document).on('click', function (e) {
+        var container = $("#fh5co-offcanvas, .js-fh5co-nav-toggle");
+        if (!container.is(e.target) && container.has(e.target).length === 0) {
+
+            if ( $('body').hasClass('offcanvas-visible') ) {
+
+                $('body').removeClass('offcanvas-visible');
+                $('.js-fh5co-nav-toggle').removeClass('active');
+            }
+        }
+        });
+    };
+
+
+
+    // Document on load
+    $(function(){
+        offcanvasMenu();
+        mainMenuSticky();
+        setFullHeight();
+        burgerMenu();
+        mobileMenuOutsideClick();
+    });
+
+}());
diff --git a/website/themes/lektor-icon/flowblocks/content.ini b/website/themes/lektor-icon/flowblocks/content.ini
new file mode 100644
index 0000000..919b9c4
--- /dev/null
+++ b/website/themes/lektor-icon/flowblocks/content.ini
@@ -0,0 +1,45 @@
+[block]
+name = Generic Content Block
+
+[fields.title]
+label = Section Title
+description = A title for this section. If this and the description is left empty, no title block is shown for the section.
+size = large
+type = string
+
+[fields.description]
+label = Section Description
+description = If present, displayed as a short block of text underneath the title. Can use Markdown formatting.
+type = markdown
+
+[fields.nav_label]
+label = Navbar Link Label
+description = Text to use as a link to the section in the top navigation bar. Typically a shorter form of the section title. If not provided, this section will not appear in the navbar.
+type = string
+
+[fields.section_id]
+label = Section ID for HTML
+description = Alphanumeric, unique identifier to use for the "id" property of this section within the HTML. Required; will appear in links to the section so should be human-readable.
+type = string
+
+[fields.content]
+label = Content
+description = Textual and image body content for the section, expressed as Markdown.
+type = markdown
+
+[fields.button_content]
+label = Button Content (Text/Image Path)
+description = Content to be displayed as/inside a button at the end of the section, if present; either HTML text to be rendered inside a button object, or an image path/URL to be used as the button directly (controlled by the Button Type setting). If not provided, no button is displayed.
+type = html
+
+[fields.button_type]
+label = Button Type
+description = If Button Content is specified, controls how it is treated to create a button. If Text, the content is rendered as HTML text inside a button object. If Image, treated as a link or path to an image, which is used as the button instead.
+type = select
+choices = text, image
+choice_labels = Text, Image
+
+[fields.button_link]
+label = Button Link
+description = URL the button should link the user to when clicked.
+type = url
diff --git a/website/themes/lektor-icon/flowblocks/gallery.ini b/website/themes/lektor-icon/flowblocks/gallery.ini
new file mode 100644
index 0000000..d8d0849
--- /dev/null
+++ b/website/themes/lektor-icon/flowblocks/gallery.ini
@@ -0,0 +1,36 @@
+[block]
+name = Gallery
+
+[fields.title]
+label = Section Title
+description = A title for this section. If this and the description is left empty, no title block is shown for the section.
+size = large
+type = string
+
+[fields.description]
+label = Section Description
+description = If present, displayed as a short block of text underneath the title. Can use Markdown formatting.
+type = markdown
+
+[fields.nav_label]
+label = Navbar Link Label
+description = Text to use as a link to the section in the top navigation bar. Typically a shorter form of the section title. If not provided, this section will not appear in the navbar.
+type = string
+
+[fields.section_id]
+label = Section ID for HTML
+description = Alphanumeric, unique identifier to use for the "id" property of this section within the HTML. Required; will appear in links to the section so should be human-readable.
+type = string
+
+[fields.show_titles]
+label = Show Item Titles Above Images
+description = If true, each gallery item will have its title displayed above its image. Otherwise, gallery item titles are only shown on mouseover, and gallery images themselves are borderless.
+type = boolean
+checkbox_label = Show titles of gallery items above images
+default = false
+
+[fields.gallery_items]
+label = Gallery Items
+description = The set of flowblocks to display, each one image in the gallery.
+type = flow
+flow_blocks = gallery_item
diff --git a/website/themes/lektor-icon/flowblocks/gallery_item.ini b/website/themes/lektor-icon/flowblocks/gallery_item.ini
new file mode 100644
index 0000000..d141c14
--- /dev/null
+++ b/website/themes/lektor-icon/flowblocks/gallery_item.ini
@@ -0,0 +1,30 @@
+[block]
+name = Gallery Item
+
+[fields.title]
+label = Title
+description = The title of the gallery item, displayed overlayed on the image on mouseover, and optionally at all times above it.
+string = string
+
+[fields.subtitle]
+label = Subtitle
+description = A short subtitle for the gallery item, displayed overlayed on the image. Plain text.
+string = string
+
+[fields.image]
+label = Image
+description = The image file to display as the gallery item, and possibly larger in a popup view if Direct Link is disabled.
+type = select
+source = record.attachments.images
+
+[fields.url]
+label = URL
+description = A URL to link the upper title to (if Show Item Titles Above Images is enabled) as well as clicking the image, if Direct Link is enabled. If neither is enabled, this is unused. Not needed if neither is enabled.
+type = url
+
+[fields.direct_link]
+label = Direct Link
+description = If checked, clicking the image will open the URL instead of opening the image itself in a popup viewer.
+type = boolean
+checkbox_label = Open link on click instead of image popup
+default = false
diff --git a/website/themes/lektor-icon/flowblocks/member.ini b/website/themes/lektor-icon/flowblocks/member.ini
new file mode 100644
index 0000000..b725635
--- /dev/null
+++ b/website/themes/lektor-icon/flowblocks/member.ini
@@ -0,0 +1,53 @@
+[block]
+name = Team Member
+
+[fields.name]
+label = Name
+description = The name of the team member, displayed below the image.
+type = string
+
+[fields.position]
+label = Position/Role
+description = The position, role or other short label for the team member. Optional.
+type = string
+
+[fields.description]
+label = Description
+description = A short description or biography for the team member, displayed near the bottom of the card. Optional; can use Markdown formatting.
+type = markdown
+
+[fields.image]
+label = Image
+description = A photo or other image of the team member. If not provided, a generic missing person image is displayed in its place.
+type = select
+source = record.attachments.images
+
+[fields.website]
+label = Website URL
+description = A link to the team member's website; optional.
+type = url
+
+[fields.facebook]
+label = Facebook URL
+description = A link to the team member's Facebook page; optional.
+type = url
+
+[fields.twitter]
+label = Twitter URL
+description = A link to the team member's Twitter feed; optional.
+type = url
+
+[fields.instagram]
+label = Instagram URL
+description = A link to the team member's Instagram gallery; optional.
+type = url
+
+[fields.github]
+label = Github URL
+description = A link to the team member's Github account; optional.
+type = url
+
+[fields.linkedin]
+label = LinkedIn URL
+description = A link to the team member's Linkdin profile; optional.
+type = url
diff --git a/website/themes/lektor-icon/flowblocks/mission.ini b/website/themes/lektor-icon/flowblocks/mission.ini
new file mode 100644
index 0000000..c2d1d23
--- /dev/null
+++ b/website/themes/lektor-icon/flowblocks/mission.ini
@@ -0,0 +1,41 @@
+[block]
+name = Mission Block
+
+[fields.title]
+label = Section Title
+description = A title for this section. If this and the description is left empty, no title block is shown for the section.
+size = large
+type = string
+
+[fields.description]
+label = Section Description
+description = If present, displayed as a short block of text underneath the title. Can use Markdown formatting.
+type = markdown
+
+[fields.nav_label]
+label = Navbar Link Label
+description = Text to use as a link to the section in the top navigation bar. Typically a shorter form of the section title. If not provided, this section will not appear in the navbar.
+type = string
+
+[fields.section_id]
+label = Section ID for HTML
+description = Alphanumeric, unique identifier to use for the "id" property of this section within the HTML. Required; will appear in links to the section so should be human-readable.
+type = string
+
+[fields.enable_images]
+label = Enable Images
+description = If checked, will display a column with each mission's image to the right of its text. Otherwise, will display only the mission text and the tab bar full-width.
+type = boolean
+default = true
+checkbox_label = Enable mission images column
+
+[fields.initial_tab_index]
+label = Initially Opened Tab
+description = The integer index (in the order flowblocks/tabs are listed, starting with 1) of the tab to display when the page is first opened. By default, the first tab (index 1).
+type = integer
+
+[fields.items]
+label = Mission Tabs
+description = Flowblocks, each a mission tab item, of the mission tabs to display in this section.
+type = flow
+flow_blocks = mission_tab
diff --git a/website/themes/lektor-icon/flowblocks/mission_tab.ini b/website/themes/lektor-icon/flowblocks/mission_tab.ini
new file mode 100644
index 0000000..ff45396
--- /dev/null
+++ b/website/themes/lektor-icon/flowblocks/mission_tab.ini
@@ -0,0 +1,28 @@
+[block]
+name = Mission Tab
+
+[fields.title]
+label = Tab Title
+description = A title for this mission tab; required.
+type = string
+
+[fields.subtitle]
+label = Subtitle
+description = If present, displayed as a short block of text at the top of the mission text section while this item is active.
+type = string
+
+[fields.description]
+label = Section Description
+description = If present, the main textual content of this mission tab's text section. Can use Markdown formatting.
+type = markdown
+
+[fields.section_id]
+label = Section ID for HTML
+description = Alphanumeric, unique identifier to use for the "id" property of this section within the HTML. Required; will appear in links to the section so should be human-readable.
+type = string
+
+[fields.image]
+label = Image
+description = Image to display when this mission tab is active, if Enable Images is checked for this mission tab's parent mission flowblock.
+type = select
+source = record.attachments.images
diff --git a/website/themes/lektor-icon/flowblocks/service.ini b/website/themes/lektor-icon/flowblocks/service.ini
new file mode 100644
index 0000000..0f3d437
--- /dev/null
+++ b/website/themes/lektor-icon/flowblocks/service.ini
@@ -0,0 +1,18 @@
+[block]
+name = Service Item
+
+[fields.title]
+label = Section Title
+description = A title for this service item. Can be omitted if desired.
+type = string
+
+[fields.description]
+label = Section Description
+description = If present, a short block of text below the title describing this service/item. Can use Markdown formatting.
+type = markdown
+
+[fields.icon]
+label = Image/Icon
+description = Image or icon to display near the top of the service card, representing it. Can be omitted.
+type = select
+source = record.attachments.images
diff --git a/website/themes/lektor-icon/flowblocks/services.ini b/website/themes/lektor-icon/flowblocks/services.ini
new file mode 100644
index 0000000..cbc57e1
--- /dev/null
+++ b/website/themes/lektor-icon/flowblocks/services.ini
@@ -0,0 +1,34 @@
+[block]
+name = Services Block
+
+[fields.title]
+label = Section Title
+description = A title for this section. If this and the description is left empty, no title block is shown for the section.
+size = large
+type = string
+
+[fields.description]
+label = Section Description
+description = If present, displayed as a short block of text underneath the title. Can use Markdown formatting.
+type = markdown
+
+[fields.nav_label]
+label = Navbar Link Label
+description = Text to use as a link to the section in the top navigation bar. Typically a shorter form of the section title. If not provided, this section will not appear in the navbar.
+type = string
+
+[fields.section_id]
+label = Section ID for HTML
+description = Alphanumeric, unique identifier to use for the "id" property of this section within the HTML. Required; will appear in links to the section so should be human-readable.
+type = string
+
+[fields.video_url]
+label = Services Popup Video URL
+description = A full URL to a remotely hosted video, e.g. on Youtube. If provided, will display a video play icon above the services section that will pop up into an embedded player.
+type = url
+
+[fields.services]
+label = Services Cards
+description = Flowblocks, each of a services card item, to display as the primary content of this section.
+type = flow
+flow_blocks = service
diff --git a/website/themes/lektor-icon/flowblocks/team.ini b/website/themes/lektor-icon/flowblocks/team.ini
new file mode 100644
index 0000000..32b4210
--- /dev/null
+++ b/website/themes/lektor-icon/flowblocks/team.ini
@@ -0,0 +1,29 @@
+[block]
+name = Team Listing
+
+[fields.title]
+label = Section Title
+description = A title for this section. If this and the description is left empty, no title block is shown for the section.
+size = large
+type = string
+
+[fields.description]
+label = Section Description
+description = If present, displayed as a short block of text underneath the title. Can use Markdown formatting.
+type = markdown
+
+[fields.nav_label]
+label = Navbar Link Label
+description = Text to use as a link to the section in the top navigation bar. Typically a shorter form of the section title. If not provided, this section will not appear in the navbar.
+type = string
+
+[fields.section_id]
+label = Section ID for HTML
+description = Alphanumeric, unique identifier to use for the "id" property of this section within the HTML. Required; will appear in links to the section so should be human-readable.
+type = string
+
+[fields.members]
+label = Team Members
+description = Flowblocks, each representing a team member, to display as the primary content of this section.
+type = flow
+flow_blocks = member
diff --git a/website/themes/lektor-icon/images/blog-index.png b/website/themes/lektor-icon/images/blog-index.png
new file mode 100644
index 0000000..ce1027c
Binary files /dev/null and b/website/themes/lektor-icon/images/blog-index.png differ
diff --git a/website/themes/lektor-icon/images/full-blog.png b/website/themes/lektor-icon/images/full-blog.png
new file mode 100644
index 0000000..44d499c
Binary files /dev/null and b/website/themes/lektor-icon/images/full-blog.png differ
diff --git a/website/themes/lektor-icon/images/full-page.png b/website/themes/lektor-icon/images/full-page.png
new file mode 100644
index 0000000..abc0010
Binary files /dev/null and b/website/themes/lektor-icon/images/full-page.png differ
diff --git a/website/themes/lektor-icon/images/gallery-404.png b/website/themes/lektor-icon/images/gallery-404.png
new file mode 100644
index 0000000..731f630
Binary files /dev/null and b/website/themes/lektor-icon/images/gallery-404.png differ
diff --git a/website/themes/lektor-icon/images/gallery-singlepage.png b/website/themes/lektor-icon/images/gallery-singlepage.png
new file mode 100644
index 0000000..58fa7c6
Binary files /dev/null and b/website/themes/lektor-icon/images/gallery-singlepage.png differ
diff --git a/website/themes/lektor-icon/images/mainpage-screenshots.png b/website/themes/lektor-icon/images/mainpage-screenshots.png
new file mode 100644
index 0000000..d258295
Binary files /dev/null and b/website/themes/lektor-icon/images/mainpage-screenshots.png differ
diff --git a/website/themes/lektor-icon/images/responsive-layout.png b/website/themes/lektor-icon/images/responsive-layout.png
new file mode 100644
index 0000000..6f5e299
Binary files /dev/null and b/website/themes/lektor-icon/images/responsive-layout.png differ
diff --git a/website/themes/lektor-icon/models/404.ini b/website/themes/lektor-icon/models/404.ini
new file mode 100644
index 0000000..d8b8ec7
--- /dev/null
+++ b/website/themes/lektor-icon/models/404.ini
@@ -0,0 +1,35 @@
+[model]
+name = 404 Error Page
+label = {{ this.full_title }}
+hidden = true
+
+[children]
+enabled = false
+
+[fields.full_title]
+label = Error Page Title
+description = Title to include on the 404 page itself.
+size = large
+type = string
+
+[fields.short_title]
+label = Tab (Short) Title
+description = String to show before "— Your Site Name" in the tab title.
+type = string
+
+[fields.bg_image]
+label = Error Page Background Image
+description = Full page ("size: cover") background image for the 404 page.
+type = select
+source = record.attachments.images
+
+[fields.bg_image_fadeout]
+label = Background Image Fadeout Fraction
+description = Transparency of the image (i.e. amount faded to white).
+addon_label = Real number between 0 (no fade) to 1 (full white)
+type = float
+
+[fields.message]
+label = Error Message
+description = The error message to display, as Markdown text.
+type = markdown
diff --git a/website/themes/lektor-icon/models/author.ini b/website/themes/lektor-icon/models/author.ini
new file mode 100644
index 0000000..f6689c0
--- /dev/null
+++ b/website/themes/lektor-icon/models/author.ini
@@ -0,0 +1,19 @@
+[model]
+name = Author
+label = {{ this.name }}
+hidden = true
+
+[children]
+enabled = false
+
+[fields.name]
+label = Author Name
+description = Full name of the author, as it should be displayed on the site.
+size = large
+type = string
+
+[fields.image]
+label = Author Image
+description = If present, image used as the author's icon, e.g. in blog posts.
+type = select
+source = record.attachments.images
diff --git a/website/themes/lektor-icon/models/authors.ini b/website/themes/lektor-icon/models/authors.ini
new file mode 100644
index 0000000..6b738c6
--- /dev/null
+++ b/website/themes/lektor-icon/models/authors.ini
@@ -0,0 +1,11 @@
+[model]
+name = Authors
+label = Authors List
+hidden = true
+protected = true
+
+[children]
+enabled = true
+hidden = false
+model = author
+order_by = name
diff --git a/website/themes/lektor-icon/models/blog-post.ini b/website/themes/lektor-icon/models/blog-post.ini
new file mode 100644
index 0000000..580926e
--- /dev/null
+++ b/website/themes/lektor-icon/models/blog-post.ini
@@ -0,0 +1,51 @@
+[model]
+name = Blog Post
+label = {{ this.title }}
+hidden = true
+
+[children]
+enabled = false
+
+[fields.title]
+label = Title
+description = Title of the blog post. Used on the page and in the tab title.
+size = large
+type = string
+
+[fields.author]
+label = Author
+description = The post's author from the site's authors database; will display their formatted name and picture. Add a new one under the Authors List page.
+type = select
+source = site.query('/authors')
+
+[fields.category]
+label = Category
+description = The overall type of post. Not currently displayed publicly, but can be used internally for organizing posts and may be implemented in the future.
+type = select
+choices = Announcement, News, Event, Tutorial, Opinion, Other
+
+[fields.tags]
+label = Tags
+description = Tags are similar to categories, but they are generally used to describe your post in more detail. Not currently displayed publicly, but can be used internally for organizing posts and may be implemented in the future.
+type = strings
+
+[fields.summary]
+label = Summary
+description = A short, few-sentence summary of the post's contents that will be displayed on the main Blog Index page to wet readers' appetites. No markdown formatting.
+type = string
+
+[fields.pub_date]
+label = Publication date
+description = When should this post be displayed as published? Also used for sorting on the index page.
+type = datetime
+
+[fields.allow_comments]
+label = Allow Comments
+description = Whether to enable Disqus comments on the selected post (requires the lektor-disqus-comments plugin). Will be set to the index-page-level default if not set.
+type = boolean
+checkbox_label = Enable comments on this post
+
+[fields.body]
+label = Body
+description = Body text for the blog post, formatted as Markdown.
+type = markdown
diff --git a/website/themes/lektor-icon/models/blog.ini b/website/themes/lektor-icon/models/blog.ini
new file mode 100644
index 0000000..5f2ae3f
--- /dev/null
+++ b/website/themes/lektor-icon/models/blog.ini
@@ -0,0 +1,31 @@
+[model]
+name = Blog Index
+label = Blog Index Page
+hidden = true
+protected = true
+
+[children]
+model = blog-post
+order_by = -pub_date, title
+
+[pagination]
+enabled = true
+per_page = 10
+
+[fields.title]
+label = Title
+description = Title at the top of the blog index page (e.g. "Recent Posts").
+size = large
+type = string
+
+[fields.sort_key]
+label = Sort Key
+description = Integer determining the order of pages in the navbar (ascending).
+type = sort_key
+
+[fields.allow_comments_default]
+label = Enable Comments by Default
+description = Enable Disqus comments on blog posts; can be overridden per-post.
+type = boolean
+checkbox_label = Enable comments by default (requires lektor-disqus-comments)
+default = false
diff --git a/website/themes/lektor-icon/models/page.ini b/website/themes/lektor-icon/models/page.ini
new file mode 100644
index 0000000..108fbba
--- /dev/null
+++ b/website/themes/lektor-icon/models/page.ini
@@ -0,0 +1,24 @@
+[model]
+name = Generic Page
+label = {{ this.full_title }}
+
+[fields.full_title]
+label = Page Title
+description = Title to include on the content page itself.
+size = large
+type = string
+
+[fields.short_title]
+label = Tab (Short) Title
+description = String to show before "— Your Site Name" in the tab title.
+type = string
+
+[fields.sort_key]
+label = Sort Key
+description = Integer determining the order of pages in the navbar (ascending).
+type = sort_key
+
+[fields.body]
+label = Body Text
+description = Body text of the content page, formatted as Markdown.
+type = markdown
diff --git a/website/themes/lektor-icon/models/single-layout.ini b/website/themes/lektor-icon/models/single-layout.ini
new file mode 100644
index 0000000..914d714
--- /dev/null
+++ b/website/themes/lektor-icon/models/single-layout.ini
@@ -0,0 +1,44 @@
+[model]
+name = Single Page Layout
+label = {{ this.title }}
+protected = true
+
+[children]
+order_by: sort_key, _id
+
+[fields.show_home_nav]
+label = Show Home in Navbar
+description = If checked, display a "Home" link to scroll to the page top.
+type = boolean
+default = true
+checkbox_label = Show "Home" link in the top navigation bar
+
+[fields.hero_title]
+label = Hero Textbox Title
+description = Title to overlay on the hero image. None shown if blank.
+size = large
+type = string
+
+[fields.hero_description]
+label = Hero Textbox Description
+description = Short description/catchphrase of the page; shown under the title if present. Can include HTML formatting.
+type = html
+
+[fields.hero_image]
+label = Hero Background Image
+description = Full page (size: cover) background image for the first section.
+type = select
+source = record.attachments.images
+
+[fields.starting_block_bg]
+label = Starting Body Block Background
+description = Set the background of the first content flowblock (further backgrounds alternate).
+type = select
+choices = dark, light
+choice_labels = Dark, Light
+
+[fields.main_content]
+label = Main Content Flowblocks
+description = The set of flowblock sections comprising the body content of the page.
+type = flow
+flow_blocks = mission, content, services, gallery, team
diff --git a/website/themes/lektor-icon/templates/404.html b/website/themes/lektor-icon/templates/404.html
new file mode 100644
index 0000000..8f27be8
--- /dev/null
+++ b/website/themes/lektor-icon/templates/404.html
@@ -0,0 +1,32 @@
+{% extends "page.html" %}
+{% block stylesheets %}
+<style>
+  {% if this.bg_image %}
+  {% set error_img_file = this.attachments.get(this.bg_image) %}
+  {% set is_svg = this.bg_image.split('.')[-1].lower() in ['svg', 'svgz'] %}
+  {% if error_img_file.thumbnail %}
+  {% if this.bg_image_fadeout and (this.bg_image_fadeout >= 0.0) and (this.bg_image_fadeout <= 1.0) %}
+    {% set bg_image_fadeout = this.bg_image_fadeout %}
+  {% else %}
+    {% set bg_image_fadeout = 0 %}
+  {% endif %}
+  #error-page {
+    background-image: linear-gradient(rgba(255, 255, 255, {{ bg_image_fadeout }}), rgba(255, 255, 255, {{ bg_image_fadeout }})), url("{{ error_img_file.thumbnail(width=1920) | url }}");
+  }
+  @media ((min-device-width: 1921px) or ((min-device-width: 1281px) and (min-resolution: 102dpi)) or ((min-device-width: 961px) and (min-resolution: 152dpi))) {
+    #error-page {
+      background-image: linear-gradient(rgba(255, 255, 255, {{ bg_image_fadeout }}), rgba(255, 255, 255, {{ bg_image_fadeout }})), url("{{ error_img_file.thumbnail(width=3840) | url }}")
+    }
+  }
+  {% elif is_svg %}
+  #error-page {
+    background-image: url("{{ this.bg_image | url }}");
+  }
+  {% endif %}
+  {% endif %}
+</style>
+{% endblock %}
+{% block pageid %}error-page{% endblock %}
+{% block pagebody %}
+{{ this.message }}
+{% endblock %}
diff --git a/website/themes/lektor-icon/templates/author.html b/website/themes/lektor-icon/templates/author.html
new file mode 100644
index 0000000..026be50
--- /dev/null
+++ b/website/themes/lektor-icon/templates/author.html
@@ -0,0 +1,5 @@
+{% extends "blog-layout.html" %}
+
+{% block blogbody %}
+
+{% endblock %}
diff --git a/website/themes/lektor-icon/templates/authors.html b/website/themes/lektor-icon/templates/authors.html
new file mode 100644
index 0000000..7e7942e
--- /dev/null
+++ b/website/themes/lektor-icon/templates/authors.html
@@ -0,0 +1 @@
+{% extends "none.html" %}
diff --git a/website/themes/lektor-icon/templates/blocks/content.html b/website/themes/lektor-icon/templates/blocks/content.html
new file mode 100644
index 0000000..26d30b9
--- /dev/null
+++ b/website/themes/lektor-icon/templates/blocks/content.html
@@ -0,0 +1,16 @@
+{% if this.content %}
+<div class="center">
+  {{ this.content }}
+</div>
+{% endif %}
+{% if this.button_type and this.button_content %}
+<div class="row">
+  {% if this.button_type == "text" %}
+  <a href="{{ this.button_link }}"  class="content-button text-button" target="_blank" rel="noopener noreferrer">{{ this.button_content | safe }}</a>
+  {% elif this.button_type == "image" %}
+  <a href="{{ this.button_link }}" class="center content-button image-button" target="_blank" rel="noopener noreferrer">
+    <img alt="Action Button" src="{{ this.button_content | url }}" class="center">
+  </a>
+  {% endif %}
+</div>
+{% endif %}
diff --git a/website/themes/lektor-icon/templates/blocks/gallery.html b/website/themes/lektor-icon/templates/blocks/gallery.html
new file mode 100644
index 0000000..8421ee5
--- /dev/null
+++ b/website/themes/lektor-icon/templates/blocks/gallery.html
@@ -0,0 +1,34 @@
+{% if this.gallery_items %}
+{% for gallery in this.gallery_items.blocks %}
+{% if gallery.image %}
+{% set gallery_img_file = record.attachments.get(gallery.image) %}
+{% set is_svg = gallery.image.split('.')[-1].lower() in ['svg', 'svgz'] %}
+{% endif %}
+{% set gallery_class_string = 'fh5co-item' %}
+{% if gallery.url and gallery.url.url != '' and (gallery.direct_link or not gallery.image) %}
+{% set gallery_link = gallery.url %}
+{% elif gallery.image and (gallery_img_file.thumbnail or is_svg) %}
+{% set gallery_link = gallery.image %}
+{% set gallery_class_string = gallery_class_string ~ ' image-popup' %}
+{% else %}
+{% set gallery_link = '' %}
+{% endif %}
+{% if this.show_titles %}
+{% set gallery_class_string = gallery_class_string ~ ' gallery-item-title' %}
+{% endif %}
+<a href="{{ gallery_link }}" class="{{ gallery_class_string }}"{% if gallery.image and (gallery_img_file.thumbnail or is_svg) %} style="background-image: url({% if gallery_img_file.thumbnail %}{{ gallery_img_file.thumbnail(width=1024) | url }}{% else %}{{ gallery.image | url }}{% endif %})"{% endif %}>
+  {% if this.show_titles %}
+  <h3>{{ gallery.title }}</h3>
+  {% endif %}
+  <div class="fh5co-overlay"></div>
+  <div class="fh5co-copy">
+    <div class="fh5co-copy-inner">
+      <h2>{{ gallery.title }}</h2>
+      {% if gallery.subtitle %}
+      <h3>{{ gallery.subtitle }}</h3>
+      {% endif %}
+    </div>
+  </div>
+</a>
+{% endfor %}
+{% endif %}
diff --git a/website/themes/lektor-icon/templates/blocks/mission.html b/website/themes/lektor-icon/templates/blocks/mission.html
new file mode 100644
index 0000000..01e86ac
--- /dev/null
+++ b/website/themes/lektor-icon/templates/blocks/mission.html
@@ -0,0 +1,58 @@
+{% if this.items %}
+{% if this.enable_images %}
+<div class="fh5co-2col left">
+  <div class="fh5co-2col-inner left">
+{% else %}
+<div class="mission-1col-container">
+  <div class="center">
+{% endif %}
+    {% if this.initial_tab_index %}
+    {% set initial_tab_index = this.initial_tab_index %}
+    {% else %}
+    {% set initial_tab_index = 1 %}
+    {% endif %}
+    <div class="fh5co-tabs-container">
+      <ul class="fh5co-tabs">
+        {% for mission_item in this.items.blocks %}
+        {% if loop.index == initial_tab_index %}
+        <li class="active">
+        {% else %}
+        <li>
+        {% endif %}
+          <a href="#" data-tab="{{ mission_item.section_id }}">{{ mission_item.title }}</a>
+        </li>
+        {% endfor %}
+      </ul>
+
+      {% for mission_item in this.items.blocks %}
+      {% if loop.index == initial_tab_index %}
+      {% set active_class = ' active' %}
+      {% else %}
+      {% set active_class = '' %}
+      {% endif %}
+      <div class="fh5co-tab-content{{ active_class }}" data-tab-content="{{ mission_item.section_id }}">
+        <h2>{{ mission_item.subtitle }}</h2>
+        {{ mission_item.description }}
+      </div>
+      {% endfor %}
+    </div>
+  </div>
+</div>
+
+{% if this.enable_images %}
+<div class="fh5co-2col right">
+  <div class="mission-image-container">
+    {% for mission_item in this.items.blocks %}
+    {% if loop.index == initial_tab_index %}
+    {% set active_class = ' active' %}
+    {% else %}
+    {% set active_class = '' %}
+    {% endif %}
+    {% if mission_item.image %}
+    <div class="mission-image tab-image {{ active_class }}" data-tab-content="{{ mission_item.section_id }}" style="background-image: url({{ mission_item.image | url }});"></div>
+    {% endif %}
+    {% endfor %}
+  </div>
+</div>
+{% endif %}
+{% endif %}
diff --git a/website/themes/lektor-icon/templates/blocks/services.html b/website/themes/lektor-icon/templates/blocks/services.html
new file mode 100644
index 0000000..7045be1
--- /dev/null
+++ b/website/themes/lektor-icon/templates/blocks/services.html
@@ -0,0 +1,31 @@
+{% if this.video_url and this.video_url.url != '' %}
+<div class="fh5co-video">
+  <a href="{{ this.video_url }}" class="popup-youtube"><i class="icon-play2"></i></a>
+  <span>Watch video</span>
+</div>
+{% endif %}
+{% if this.services %}
+<div class="container">
+  {% for service in this.services.blocks %}
+  <div class="col-md-4 card-outer">
+    <div class="card service">
+      <div class="card-inner">
+        {% if service.icon %}
+        {% set service_img_file = record.attachments.get(service.icon) %}
+        <div class="icon">
+          {% if service_img_file.thumbnail %}
+          <img alt="{{ service.title | striptags | striptags }} photo" src="{{ service_img_file.thumbnail(width=640) | url }}" class="img-responsive center" srcset="{{ service_img_file.thumbnail(width=640) | url }} 1x, {{ service_img_file.thumbnail(width=1280) | url }} 2x">
+          {% elif service.icon.split('.')[-1].lower() in ['svg', 'svgz'] %}
+          <img alt="{{ service.title | striptags | striptags }} photo" src="{{ service.icon | url }}" class="img-responsive center">
+          {% endif %}
+        </div>
+        {% endif %}
+        <h3>{{ service.title }}</h3>
+        {{ service.description }}
+      </div>
+      <div class="card-spacer"></div>
+    </div>
+  </div>
+  {% endfor %}
+</div>
+{% endif %}
diff --git a/website/themes/lektor-icon/templates/blocks/team.html b/website/themes/lektor-icon/templates/blocks/team.html
new file mode 100644
index 0000000..67d907a
--- /dev/null
+++ b/website/themes/lektor-icon/templates/blocks/team.html
@@ -0,0 +1,46 @@
+{% if this.members %}
+<div class="container">
+  {% for member in this.members.blocks %}
+  <div class="col-md-4 card-outer">
+    <div class="card person">
+      <div class="card-inner">
+        {% if member.image %}
+        {% set member_img_file = record.attachments.get(member.image) %}
+        {% endif %}
+        {% if member.image and member_img_file.thumbnail %}
+        <img alt="{{ member.name | striptags | striptags }} photo" src="{{ member_img_file.thumbnail(width=512) | url }}" class="img-responsive" srcset="{{ member_img_file.thumbnail(width=512) | url }} 1x, {{ member_img_file.thumbnail(width=1024) | url }} 2x">
+        {% elif member.image and member.image.split('.')[-1].lower() in ['svg', 'svgz'] %}
+        <img alt="{{ member.name | striptags | striptags }} photo" src="{{ member.image | url }}" class="img-responsive">
+        {% else %}
+        <img alt="Placeholder person" src="{{ '/static/images/placeholder_person.png' | asseturl }}" class="img-responsive">
+        {% endif %}
+        <h3>{{ member.name }}</h3>
+        <h4>{{ member.position }}</h4>
+        {{ member.description }}
+        <ul class="social">
+          {% if  member.website and member.website.url != '' %}
+          <li><a href="{{ member.website }}" aria-label="Website"><i class="icon-globe2"></i></a></li>
+          {% endif %}
+          {% if member.facebook and member.facebook.url != '' %}
+          <li><a href="{{ member.facebook }}" aria-label="Facebook"><i class="icon-facebook"></i></a></li>
+          {% endif %}
+          {% if member.twitter and member.twitter.url != '' %}
+          <li><a href="{{ member.twitter }}" aria-label="Twitter"><i class="icon-twitter"></i></a></li>
+          {% endif %}
+          {% if member.instagram and member.instagram.url != '' %}
+          <li><a href="{{ member.instagram }}" aria-label="Instagram"><i class="icon-instagram"></i></a></li>
+          {% endif %}
+          {% if member.github and member.github.url != '' %}
+          <li><a href="{{ member.github }}" aria-label="Github"><i class="icon-github"></i></a></li>
+          {% endif %}
+          {% if member.github and member.linkedin.url != '' %}
+          <li><a href="{{ member.linkedin }}" aria-label="Linkedin"><i class="icon-linkedin"></i></a></li>
+          {% endif %}
+        </ul>
+      </div>
+      <div class="card-spacer"></div>
+    </div>
+  </div>
+  {% endfor %}
+</div>
+{% endif %}
diff --git a/website/themes/lektor-icon/templates/blog-layout.html b/website/themes/lektor-icon/templates/blog-layout.html
new file mode 100644
index 0000000..e7d9c5c
--- /dev/null
+++ b/website/themes/lektor-icon/templates/blog-layout.html
@@ -0,0 +1,17 @@
+{% extends "layout.html" %}
+{% block section %}<span class="pipe-colored">|</span> Blog{% endblock %}
+{% block nav_main %}
+{% if config.THEME_SETTINGS.atom_enable and config.THEME_SETTINGS.atom_enable.lower() not in ["false", "f", "no", "n"] %}
+<a id="rss-nav-link" data-nav-section="RSS" href="{{ '/blog/feed.xml' | url }}">RSS</a>
+{% endif %}
+{% endblock %}
+{% block body %}
+<div class="blog-content-container" data-section="Blog">
+  {% block blogmaintitle %}
+  {% endblock %}
+  <div id="blog-center" class="center">
+    {% block blogbody %}
+    {% endblock %}
+  </div>
+</div>
+{% endblock %}
diff --git a/website/themes/lektor-icon/templates/blog-post.html b/website/themes/lektor-icon/templates/blog-post.html
new file mode 100644
index 0000000..b2c383a
--- /dev/null
+++ b/website/themes/lektor-icon/templates/blog-post.html
@@ -0,0 +1,12 @@
+{% extends "blog-layout.html" %}
+{% from "macros/blog.html" import render_blog_post %}
+{% block title %}Blog{% if this.title %} | {{ this.title }}{% endif %}{% endblock %}
+{% block blogbody %}
+{{ render_blog_post(this) }}
+{% if config.THEME_SETTINGS.disqus_name and not this.allow_comments == False and (this.allow_comments or this.parent.allow_comments_default) %}
+<div class="comment-box">
+  <h2>Comments</h2>
+  {{ render_disqus_comments() }}
+</div>
+{% endif %}
+{% endblock %}
diff --git a/website/themes/lektor-icon/templates/blog.html b/website/themes/lektor-icon/templates/blog.html
new file mode 100644
index 0000000..8012bff
--- /dev/null
+++ b/website/themes/lektor-icon/templates/blog.html
@@ -0,0 +1,22 @@
+{% extends "blog-layout.html" %}
+{% from "macros/blog.html" import render_blog_post %}
+{% from "macros/pagination.html" import render_pagination %}
+
+{% block title %}Blog{% endblock %}
+
+{% block blogmaintitle %}
+{% if this.title %}
+<div id="blog-main-title" class="text-center fh5co-heading">
+  <h1>{{ this.title }}</h1>
+</div>
+{% endif %}
+{% endblock %}
+
+{% block blogbody %}
+{% if this.pagination.items %}
+{% for child in this.pagination.items %}
+{{ render_blog_post(child, from_index=true) }}
+{% endfor %}
+{% endif %}
+{{ render_pagination(this.pagination) }}
+{% endblock %}
diff --git a/website/themes/lektor-icon/templates/layout.html b/website/themes/lektor-icon/templates/layout.html
new file mode 100644
index 0000000..edc1a4d
--- /dev/null
+++ b/website/themes/lektor-icon/templates/layout.html
@@ -0,0 +1,266 @@
+<!DOCTYPE html>
+{% if config.THEME_SETTINGS.content_lang %}
+{% set lang_string = ' lang="' ~ config.THEME_SETTINGS.content_lang ~ '"' %}
+{% else %}
+{% set lang_string = '' %}
+{% endif %}
+<html{{ lang_string | safe }}>
+<head>
+  <meta charset="UTF-8">
+  <meta http-equiv="Access-Control-Allow-Origin" content="Origin">
+  {% block csp %}
+  {% if not config.THEME_SETTINGS.content_security_policy or config.THEME_SETTINGS.content_security_policy in ["true", "t", "yes", "y"] %}
+  <meta http-equiv="Content-Security-Policy" content="
+{% set frame_src = "'self' http: https:" %}
+{% set img_src = "'self' http: https: data:" %}
+{% set script_src = "'self' 'unsafe-inline'" %}
+{% set style_src = "'self' https://fonts.googleapis.com 'unsafe-inline'" %}
+
+{% if config.THEME_SETTINGS.disqus_name %}
+{% set script_src = script_src ~ " " ~ "https://disqus.com https://*.disqus.com https://*.disquscdn.com http://" ~ config.THEME_SETTINGS.disqus_name ~ ".disqus.com" %}
+{% set style_src = style_src ~ " " ~ "https://disqus.com https://*.disqus.com https://*.disquscdn.com http://" ~ config.THEME_SETTINGS.disqus_name ~ ".disqus.com" %}
+{% endif %}
+
+{% if config.THEME_SETTINGS.gitter_room %}
+{% set script_src = script_src ~ " " ~ "https://gitter.im https://*.gitter.im" %}
+{% set style_src = style_src ~ " " ~ "https://gitter.im https://*.gitter.im" %}
+{% endif %}
+
+{% if config.THEME_SETTINGS.content_security_policy_frame_src %}
+{% set frame_src = config.THEME_SETTINGS.content_security_policy_frame_src %}
+{% endif %}
+{% if config.THEME_SETTINGS.content_security_policy_script_src %}
+{% set script_src = config.THEME_SETTINGS.content_security_policy_script_src %}
+{% endif %}
+{% if config.THEME_SETTINGS.content_security_policy_style_src %}
+{% set style_src = config.THEME_SETTINGS.content_security_policy_style_src %}
+{% endif %}
+
+base-uri 'self';
+default-src 'self';
+font-src 'self' https://fonts.gstatic.com;
+frame-src {{ frame_src }};
+img-src {{ img_src }};
+plugin-types application/pdf;
+script-src {{ script_src }};
+style-src {{ style_src }};
+">
+  {% elif config.THEME_SETTINGS.content_security_policy.lower() not in ["false", "f", "no", "n"] %}
+  <meta http-equiv="Content-Security-Policy" content="{{ config.THEME_SETTINGS.content_security_policy | safe }}">
+  {% endif %}
+  {% endblock %}
+  <meta http-equiv="X-Content-Type-Options" content="nosniff">
+  <meta http-equiv="X-XSS-Protection" content="1">
+  <meta name="referrer" content="never">  <!-- Fallback for IE/Edge/Safari -->
+  <meta name="referrer" content="no-referrer">  <!-- Fallback for Chrome lt 61 -->
+  <meta name="referrer" content="same-origin">
+
+  <!-- FREE HTML5 TEMPLATE  -->
+  <!-- DESIGNED & DEVELOPED by FREEHTML5.CO -->
+
+  <!-- Website:        https://freehtml5.co/ -->
+  <!-- Email:          info@freehtml5.co -->
+  <!-- Twitter:        https://twitter.com/fh5co -->
+  <!-- Facebook:       https://www.facebook.com/fh5co -->
+
+  <!-- HUGO TEMPLATE PORTED BY SteveLane -->
+  <!-- Website:        https://interadata.io/ -->
+
+  <!-- LEKTOR THEME PORTED BY Daniel Althviz -->
+  <!-- Website:        https://dalthviz.github.io/ -->
+
+  <!-- LEKTOR THEME MAINTAINED AND EXPANED BY Spyder Dev Team -->
+  <!-- Website:        https://github.com/spyder-ide/lektor-icon -->
+
+  <!-- Copyright (c) 2016- Lektor-Icon Contributors -->
+
+  <!-- Original standalone HTML5 theme distributed under the terms of the -->
+  <!-- Creative Commons Attribution 3.0 license -->
+  <!-- https://creativecommons.org/licenses/by/3.0/ -->
+
+  <!-- Additions, modifications and porting released under the terms of the -->
+  <!-- MIT (Expat) License: https://opensource.org/licenses/MIT -->
+  <!-- See the LICENSE.txt file for more details -->
+  <!-- https://github.com/spyder-ide/lektor-icon/blob/master/LICENSE.txt -->
+
+  <!-- For information on the included third-party assets, see NOTICE.txt -->
+  <!-- https://github.com/spyder-ide/lektor-icon/blob/master/NOTICE.txt -->
+
+  <title>{% block title %}{% endblock %}{% if config.THEME_SETTINGS.title %} — {{ config.THEME_SETTINGS.title }}{% endif %}</title>
+  {% if config.THEME_SETTINGS.author %}
+  <meta name="author" content="{{ config.THEME_SETTINGS.author }}"{{ lang_string | safe }}>
+  {% endif %}
+  {% if config.THEME_SETTINGS.copyright %}
+  <meta name="copyright" content="{{ config.THEME_SETTINGS.copyright }}"{{ lang_string | safe }}>
+  {% endif %}
+  {% if config.THEME_SETTINGS.description %}
+  <meta name="description" content="{{ config.THEME_SETTINGS.description }}"{{ lang_string | safe }}>
+  {% endif %}
+  {% if config.THEME_SETTINGS.keywords %}
+  <meta name="keywords" content="{{ config.THEME_SETTINGS.keywords }}"{{ lang_string | safe }}>
+  {% endif %}
+  {% if config.THEME_SETTINGS.theme_accent_color %}
+  <meta name="theme-color" content="{{ config.THEME_SETTINGS.theme_accent_color | safe }}">
+  {% endif %}
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+
+  <link rel="icon" href="{{ config.THEME_SETTINGS.favicon_path | asseturl }}" type="image/png">
+  <style>
+    :root {
+      {% if config.THEME_SETTINGS.theme_accent_color %}
+      --theme-accent-color: {{ config.THEME_SETTINGS.theme_accent_color | safe }};
+      {% else %}
+      --theme-accent-color: #ee1c24;
+      {% endif %}
+      {% if config.THEME_SETTINGS.theme_pipe_color %}
+      --theme-pipe-color: {{ config.THEME_SETTINGS.theme_pipe_color | safe }}
+      {% elif config.THEME_SETTINGS.theme_accent_color %}
+      --theme-pipe-color: {{ config.THEME_SETTINGS.theme_accent_color | safe }}
+      {% else %}
+      --theme-pipe-color: #ff4c52;
+      {% endif %}
+    }
+  </style>
+  {% block stylesheets %}
+  {% endblock %}
+  <!-- Google fonts -->
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Amiri" type="text/css">
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway" type="text/css">
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inconsolata" type="text/css">
+  <!-- Theme style -->
+  <link rel="stylesheet" href="{{ '/static/css/style.css' | asseturl }}" type="text/css">
+  {% if config.THEME_SETTINGS.custom_css %}
+  <!-- User Custom CSS -->
+  <link rel="stylesheet" href="{{ config.THEME_SETTINGS.custom_css | asseturl }}" type="text/css">
+  {% endif %}
+</head>
+
+<body>
+
+{% block loader %}
+{% endblock %}
+<div class="fh5co-page">
+  <div id="fh5co-container">
+    <div class="js-sticky">
+      <div class="fh5co-main-nav navbar fixed-top">
+        <div class="container">
+          <div class="fh5co-menu-1">
+            {% if not (config.THEME_SETTINGS.nav_logo_path or config.THEME_SETTINGS.nav_logo_text) %}
+            {% set nav_logo_text = "Site Name" %}
+            {% elif config.THEME_SETTINGS.nav_logo_text %}
+            {% set nav_logo_text = config.THEME_SETTINGS.nav_logo_text %}
+            {% else %}
+            {% set nav_logo_text = '' %}
+            {% endif %}
+            <div class='pull-left'>
+              <ul class='dropdown menu' data-dropdown-menu>
+                <li id='menu-logo'>
+                  <p></p>
+                    <a href='{{ '/' | url }}'>
+                    {% if config.THEME_SETTINGS.nav_logo_path %}
+                    <img alt="Site Logo" src="{{ config.THEME_SETTINGS.nav_logo_path | asseturl }}">
+                    {% endif %}
+                    {% if nav_logo_text %}
+                    <span class="logo-text-container">
+                      <span class="logo-text">{{ nav_logo_text | safe }} {% block section %}{% endblock %}</span>
+                    </span>
+                    {% endif %}
+                  </a>
+                  <p></p>
+                </li>
+              </ul>
+            </div>
+
+            <div class="main-nav-container">
+              {% block nav_main %}{% endblock %}
+
+              {% if config.THEME_SETTINGS.nav_extralinks or (site.query('/') and (site.query('/') | selectattr('sort_key'))) %}
+              <div class="side-nav-container">
+                {% if config.THEME_SETTINGS.nav_extralinks %}
+                {% for nav_link in config.THEME_SETTINGS.nav_extralinks.replace(', ', ',').split(',') | reverse %}
+                <a class="pull-right" href="{{ nav_link.split(': ')[1] }}"> {{ nav_link.split(': ')[0] }}</a>
+                {% endfor %}
+                {% endif %}
+
+                {% if site.query('/') and (site.query('/') | selectattr('sort_key')) %}
+                {% for page in site.query('/') | selectattr('sort_key') | sort(attribute='sort_key', reverse=True) %}
+                {% if page.short_title %}
+                {% set navlink_text = page.short_title %}
+                {% else %}
+                {% set navlink_text = page.path | replace('/', '') | replace('-', ' ') | replace("_", " ") | title %}
+                {% endif %}
+                <a class="pull-right" href="{{ page.url_path | url }}">{{ navlink_text }}</a>
+                {% endfor %}
+                {% endif %}
+              </div>
+              {% endif %}
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    {% block body %}{% endblock %}
+
+    <div id="fh5co-footer">
+      <div class="container">
+        {% if config.THEME_SETTINGS.footer_links %}
+        <div class="row footer-connect-line">
+          <div class="text-center">
+            <p>
+              Connect with us on:
+              {% for footer_link in config.THEME_SETTINGS.footer_links.replace(', ', ',').split(',') %}
+              {% if loop.index > 1 %}
+              <span class="pipe-colored">|</span>
+              {% endif %}
+              <a href="{{ footer_link.split(': ')[1] }}"> {{ footer_link.split(': ')[0] }} </a>
+              {% endfor %}
+            </p>
+          </div>
+        </div>
+        {% endif %}
+        <div class="row footer-copyright">
+          <div class="text-center">
+            <p>
+              {% if config.THEME_SETTINGS.copyright %}
+              {{ config.THEME_SETTINGS.copyright | safe }}
+              {% endif %}
+              {% if config.THEME_SETTINGS.copyright and config.THEME_SETTINGS.footer_license_name %}
+              <span class="pipe-colored"> | </span>
+              {% endif %}
+              {% if config.THEME_SETTINGS.footer_license_name %}
+              Licensed <a href="{{ config.THEME_SETTINGS.footer_license_link }}" target="_blank" rel="noopener noreferrer">{{ config.THEME_SETTINGS.footer_license_name }}</a>
+              {% endif %}
+              {% if config.THEME_SETTINGS.copyright or config.THEME_SETTINGS.footer_license_name %}
+              <br>
+              {% endif %}
+              <a href="https://freehtml5.co/icon-free-website-template-using-bootstrap/" target="_blank" rel="noopener noreferrer">Template</a> designed by <a href="https://freehtml5.co/" target="_blank" rel="noopener noreferrer">FreeHTML5.co</a>  <span class="pipe-colored"> | </span> Licensed <a href="https://creativecommons.org/licenses/by/3.0/" target="_blank" rel="noopener noreferrer">CC-BY 3.0</a>
+              <br><a href="https://github.com/SteveLane/hugo-icon" target="_blank" rel="noopener noreferrer">Hugo port</a> by <a href="https://github.com/SteveLane/" target="_blank" rel="noopener noreferrer">SteveLane</a> <span class="pipe-colored">|</span> <a href="https://github.com/spyder-ide/lektor-icon" target="_blank" rel="noopener noreferrer">Lektor port</a> by <a href="https://github.com/dalthviz/" target="_blank" rel="noopener noreferrer">Dalthviz</a> and <a href="https://www.spyder-ide.org/" target="_blank" rel="noopener noreferrer">Spyder</a>
+            </p>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+{% if config.THEME_SETTINGS.gitter_room %}
+<script type="application/javascript">
+  ((window.gitter = {}).chat = {}).options = {
+  room: '{{ config.THEME_SETTINGS.gitter_room }}',
+  activationElement: '#open_chat',
+  };
+</script>
+<a href="https://gitter.im/{{ config.THEME_SETTINGS.gitter_room }}" id="open_chat" class="gitter-open-chat-button">Open Chat</a>
+<script defer src="https://sidecar.gitter.im/dist/sidecar.v1.js" type="application/javascript" charset="UTF-8"></script>
+{% endif %}
+<!-- jQuery -->
+<script src="{{ '/static/js/jquery-3.3.1.min.js' | asseturl }}" type="application/javascript" charset="UTF-8"></script>
+<!-- Waypoints -->
+<script src="{{ '/static/js/jquery.waypoints.min.js' | asseturl }}" type="application/javascript" charset="UTF-8"></script>
+<!-- Main JS -->
+<script src="{{ '/static/js/main.js' | asseturl }}" type="application/javascript" charset="UTF-8"></script>
+{% block scripts %}
+{% endblock %}
+<!-- <script src="https://code.jquery.com/jquery-migrate-3.0.1.js" type="application/javascript" charset="UTF-8"></script> -->
+</body>
+</html>
diff --git a/website/themes/lektor-icon/templates/macros/blog.html b/website/themes/lektor-icon/templates/macros/blog.html
new file mode 100644
index 0000000..68fa4d0
--- /dev/null
+++ b/website/themes/lektor-icon/templates/macros/blog.html
@@ -0,0 +1,55 @@
+{% macro render_blog_post(post, from_index=false) %}
+{% if post.author %}
+{% set author =  site.get('authors/' + post.author) %}
+{% if not author %}
+{% set author = post.author %}
+{% endif %}
+{% else %}
+{% set author = '' %}
+{% endif %}
+<div class="blog-post">
+  {% if from_index %}
+  <div class="blog-header blog-index-header">
+  {% else %}
+  <div class="blog-header blog-post-header">
+  {% endif %}
+    <div class="row">
+      {% if author.image %}
+      <div class="column meta-blog-author">
+        <img alt="{{ author.name | striptags | striptags }} photo" class="author-img" src="{{ (author.path ~ '/' ~ author.image) | url }}">
+      </div>
+      {% endif %}
+      <div class="meta-blog-index">
+        <p>
+          {% if author.name %}
+          {{ author.name }}<br>
+          {% elif author %}
+          {{ author }}<br>
+          {% endif %}
+          {% if post.pub_date %}
+          <span class="small">{{ post.pub_date.strftime('%Y-%m-%d') }}</span>
+          {% endif %}
+        </p>
+      </div>
+    </div>
+    {% if from_index %}
+    <div class="row">
+      <h2><a href="{{ post | url }}">{{ post.title }}</a></h2>
+    </div>
+    {% else %}
+    <h1>{{ post.title }}</h1>
+    {{ post.body }}
+    {% endif %}
+  </div>
+  {% if from_index %}
+  {% if post.summary %}
+  <p>
+    {{ post.summary }}
+  </p>
+  {% endif %}
+  <p class="pull-right">
+    <a href="{{ post | url }}">Read more...</a>
+  </p>
+  {% endif %}
+</div>
+{% endmacro %}
diff --git a/website/themes/lektor-icon/templates/macros/pagination.html b/website/themes/lektor-icon/templates/macros/pagination.html
new file mode 100644
index 0000000..c82164c
--- /dev/null
+++ b/website/themes/lektor-icon/templates/macros/pagination.html
@@ -0,0 +1,21 @@
+{% macro render_pagination(pagination) %}
+{% set previous = '<i class="fa fa-mail-forward fa-rotate-180 fa-lg"></i> Previous' %}
+{% set next = 'Next <i class="fa fa-mail-reply fa-rotate-180 fa-lg"></i>' %}
+
+<div class="row blog-pagination">
+  <div class="small-6 columns previous">
+    {% if pagination.has_prev %}
+    <a href="{{ pagination.prev | url }}" class="button">{{ previous | safe }}</a>
+    {% else %}
+    <span class="button disabled" aria-disabled="true">{{ previous | safe }}</span>
+    {% endif %}
+  </div>
+  <div class="small-6 columns next">
+    {% if pagination.has_next %}
+    <a href="{{ pagination.next | url }}" class="button">{{ next | safe }}</a>
+    {% else %}
+    <span class="button disabled" aria-disabled="true">{{ next | safe }}</span>
+    {% endif %}
+  </div>
+</div>
+{% endmacro %}
diff --git a/website/themes/lektor-icon/templates/none.html b/website/themes/lektor-icon/templates/none.html
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/website/themes/lektor-icon/templates/none.html
@@ -0,0 +1 @@
+
diff --git a/website/themes/lektor-icon/templates/page.html b/website/themes/lektor-icon/templates/page.html
new file mode 100644
index 0000000..848c03a
--- /dev/null
+++ b/website/themes/lektor-icon/templates/page.html
@@ -0,0 +1,22 @@
+{% extends "layout.html" %}
+{% block title %}{{ this.short_title }}{% endblock %}
+{% block stylesheets %}{% endblock %}
+{% block body %}
+<div id="{% block pageid %}generic-page{% endblock %}" class="page-content-container">
+  <div class="page-content">
+    <div class="text-center fh5co-heading">
+      {% if this.full_title %}
+      <h1>
+        {{ this.full_title }}
+      </h1>
+      {% endif %}
+      <div class="rule-under-heading"></div>
+    </div>
+    <div class="center">
+      {% block pagebody %}
+      {{ this.body }}
+      {% endblock %}
+    </div>
+  </div>
+</div>
+{% endblock %}
diff --git a/website/themes/lektor-icon/templates/single-layout.html b/website/themes/lektor-icon/templates/single-layout.html
new file mode 100644
index 0000000..d874830
--- /dev/null
+++ b/website/themes/lektor-icon/templates/single-layout.html
@@ -0,0 +1,116 @@
+{% extends "layout.html" %}
+
+{% block title %}Home{% endblock %}
+
+{% block stylesheets %}
+<!-- Magnific Popup -->
+<link rel="stylesheet" href="{{ '/static/css/magnific-popup.css' | asseturl }}" type="text/css">
+<style>
+  {% if this.hero_image %}
+  {% set hero_img_file = this.attachments.get(this.hero_image) %}
+  {% set is_svg = this.hero_image.split('.')[-1].lower() in ['svg', 'svgz'] %}
+  {% if hero_img_file.thumbnail %}
+  .hero-section {
+    background-image: url("{{ hero_img_file.thumbnail(width=1920) | url }}")
+  }
+  @media ((min-device-width: 1921px) or ((min-device-width: 1281px) and (min-resolution: 102dpi)) or ((min-device-width: 961px) and (min-resolution: 152dpi))) {
+    .hero-section {
+      background-image: url("{{ hero_img_file.thumbnail(width=3840) | url }}")
+    }
+  }
+  @media ((max-device-width: 768px) and (((max-resolution: 100dpi) and (max-device-height: 1400px)) or ((max-device-height: 1050px) and (max-resolution: 150dpi)) or (max-device-height: 700px))) {
+    .hero-section {
+      background-image: url("{{ hero_img_file.thumbnail(width=(hero_img_file.width * 700 / hero_img_file.height)) | url }}")
+    }
+  }
+  {% elif is_svg %}
+  .hero-section {
+    background-image: url("{{ this.hero_image | url }}")
+  }
+  {% endif %}
+  {% endif %}
+</style>
+{% endblock %}
+
+{% block loader %}
+{% if not config.THEME_SETTINGS.loader_enable or config.THEME_SETTINGS.loader_enable.lower() not in ["false", "f", "no", "n"] %}
+<div class="fh5co-loader"></div>
+{% endif %}
+{% endblock %}
+
+
+{% block nav_main %}
+
+{% if this.hero_image and this.show_home_nav %}
+<a href="#section-home" data-nav-section="home">Home</a>
+{% endif %}
+{% if this.main_content %}
+{% for block in this.main_content.blocks %}
+{% if block.nav_label %}
+<a href="#section-{{ block.section_id }}" data-nav-section="{{ block.section_id }}">{{ block.nav_label }}</a>
+{% endif %}
+{% endfor %}
+{% endif %}
+
+{% endblock %}
+
+{% block body %}
+
+{% if this.hero_image %}
+<div id="section-home" class="js-fullheight-home hero-section" data-section="home" data-stellar-background-ratio="0.5">
+  <div class="container">
+    <div class="col-md-6">
+      <div class="js-fullheight-home fh5co-copy">
+        <div class="js-fullheight-home fh5co-copy-inner">
+          {% if this.hero_title %}
+          <h1>{{ this.hero_title }}</h1>
+          {% endif %}
+          {% if this.hero_description %}
+          <h2>{{ this.hero_description }}</h2>
+          {% endif %}
+        </div>
+      </div>
+    </div>
+  </div>
+</div> <!-- END hero-section -->
+{% endif %}
+
+{% if this.main_content %}
+{% for block in this.main_content.blocks %}
+{% set extra_classes = '' %}
+{% if this.starting_block_bg and this.starting_block_bg == 'light' %}
+{% set extra_classes = extra_classes ~ loop.cycle(' light-bg-section', ' dark-bg-section') %}
+{% else %}
+{% set extra_classes = extra_classes ~ loop.cycle(' dark-bg-section', ' light-bg-section') %}
+{% endif %}
+{% if not block.title %}
+{% set extra_classes = extra_classes ~ ' notitle-section' %}
+{% endif %}
+<div id="section-{{ block.section_id }}" data-section="{{ block.section_id }}" class="{{ block._flowblock }}-section body-section{{ extra_classes }}">
+  {% if block.video_url and block.video_url.url != '' %}
+  <div class="video-spacer"></div>
+  {% endif %}
+  {% if block.title or block.description %}
+  <div class="text-center fh5co-heading">
+    <h2>{{ block.title }}</h2>
+    {{ block.description }}
+  </div>
+  {% endif %}
+{{ block }}
+</div> <!-- END {{ block._flowblock }}-section -->
+{% endfor %}
+{% endif %}
+
+{% endblock %}
+
+{% block scripts %}
+<!-- jQuery Easing -->
+<script src="{{ '/static/js/jquery.easing.min.js' | asseturl }}" type="application/javascript" charset="UTF-8"></script>
+<!-- Stellar Parallax -->
+<script src="{{ '/static/js/jquery.stellar.min.js' | asseturl }}" type="application/javascript" charset="UTF-8"></script>
+<!-- Magnific Popup -->
+<script src="{{ '/static/js/jquery.magnific-popup.min.js' | asseturl }}" type="application/javascript" charset="UTF-8"></script>
+<script src="{{ '/static/js/magnific-popup-options.js' | asseturl }}" type="application/javascript" charset="UTF-8"></script>
+<!-- Main JS -->
+<script src="{{ '/static/js/main-singlelayout.js' | asseturl }}" type="application/javascript" charset="UTF-8"></script>
+{% endblock %}
diff --git a/website/themes/lektor-icon/theme.ini b/website/themes/lektor-icon/theme.ini
new file mode 100644
index 0000000..8375bc2
--- /dev/null
+++ b/website/themes/lektor-icon/theme.ini
@@ -0,0 +1,25 @@
+[theme]
+name = Lektor Icon
+license = CC-BY 3.0 and MIT
+licenselink = https://github.com/spyder-ide/lektor-icon/blob/master/LICENSE.md
+description = The FreeHTML5 "Icon" theme ported Lektor by Daniel Althviz and the Spyder IDE dev team, originally based on the Hugo port by Steve Lane. The core template is a single-page, responsive layout, with sections for describing your organization and its mission, services, a gallery, your team and how visitors can download your software or donate to your cause. Also features additions including a built-in blog, generic page template, custom error page, common navbar, Gitter, Disqus and OpenCollective integration, heavy customizability, numerous fixes and improvements, and re-written for speed, extensibility, responsiveness and standards conformance.
+homepage = https://github.com/spyder-ide/lektor-icon
+tags = icon, responsive, single page, hero, personal, company, agency, blog
+features = mission, about, services, gallery, team, blog, page, download, donate
+lektor_required_version = 3.1
+
+[author]
+name = Daniel Althviz, Spyder IDE organization and Lektor-Icon contributors
+homepage = https://dalthviz.github.io/
+
+[original]
+author = Steve Lane
+name = Hugo Icon
+homepage = https://gtown-ds.netlify.com/
+repo = https://github.com/SteveLane/hugo-icon
+
+[original]
+author = FreeHTML5.co
+name = Icon
+homepage = https://freehtml5.co/
+repo = https://freehtml5.co/icon-free-website-template-using-bootstrap/