mirror of
https://github.com/Xevion/v2.xevion.dev.git
synced 2025-12-13 14:13:28 -06:00
New Post: Painting Images with IPv6
This commit is contained in:
178
_posts/2023-04-04-painting-images-with-ipv6.md
Normal file
178
_posts/2023-04-04-painting-images-with-ipv6.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
title: Painting Images with IPv6
|
||||||
|
date: 2023-04-14 13:07:43 -0500
|
||||||
|
tags: ipv6 python asyncio websocket PIL
|
||||||
|
_preview_description: Have you ever painted images with IPv6? I found out how in 30 minutes.
|
||||||
|
---
|
||||||
|
|
||||||
|
Despite how it sounds, this is not a joke. Well, maybe it is, but it's a fantastic demonstration of IPv6 addressing,
|
||||||
|
and you can join in on the fun right now too!
|
||||||
|
|
||||||
|
Some months ago, I found an interesting little site: [v6.sys42.net][place-v6]*.
|
||||||
|
|
||||||
|
> **Warning**: This site contains image curated by an unrestricted community: It is entirely out of my control what you
|
||||||
|
> will see on it, but _usually_, it's safe for work.
|
||||||
|
|
||||||
|
To give a short explanation of this site, it's similar to [/r/place][place-reddit], an online canvas where users can
|
||||||
|
place a single
|
||||||
|
pixel on a canvas every 5 minutes.
|
||||||
|
|
||||||
|
The difference between [/r/place][place-reddit] and [Place: IPv6][place-v6] is that the latter uses a human-accessible
|
||||||
|
user interface for
|
||||||
|
selecting colors and placing pixels, while the other uses IPv6 addresses.
|
||||||
|
|
||||||
|
Hold on - you might be thinking, _"Don't you mean a REST API? Or at least an HTTP webserver? Or even a TCP/UDP
|
||||||
|
socket?"_.
|
||||||
|
|
||||||
|
None of that, it's not just a normal server accessible by IPv6 exclusively - it's an IPv6 address range booked
|
||||||
|
specifically for the purpose of receiving R, G, B, X and Y arguments.
|
||||||
|
|
||||||
|
## How does it work?
|
||||||
|
|
||||||
|
To use the service, you send an ICMP packet (a `ping`) to a specific IPv6 address dictated by your arguments.
|
||||||
|
|
||||||
|
The arguments are encoded into the IPv6 address, and the service will receive and parse whatever you put into it.
|
||||||
|
|
||||||
|
> **Note**: The service has since been updated to use a different IPv6 address range, but the concept is the same.
|
||||||
|
|
||||||
|
Originally, the IPv6 address was {% ihighlight digdag %}2a06:a003:d040:SXXX:YYY:RR:GG:BB{% endihighlight %} where `XXX`
|
||||||
|
and `YYY` are the X and Y coordinates, and `RR`, `GG` and `BB` are the R, G and B values of the pixel. By substituting
|
||||||
|
you arguments into these positions, you can paint a pixel on the canvas. Lastly, the `S` represents the size of the
|
||||||
|
pixel.
|
||||||
|
Only `1` or `2` is accepted, representing a 1x1 or 2x2 pixel.
|
||||||
|
|
||||||
|
On top of this, the values are encoded in hexadecimal, so you can use the full range of 0-255 for each color without
|
||||||
|
worry.
|
||||||
|
|
||||||
|
As an example, painting the color {% ihighlight dart %}#008080{% endihighlight %} (teal) at the position `45, 445` would
|
||||||
|
be encoded as
|
||||||
|
{% ihighlight digdag %}2a06:a003:a040:102d:01bd:00:80:80{% endihighlight %}. To help you pick out the X and Y
|
||||||
|
coordinates, {% ihighlight java %}45{% endihighlight %} is {% ihighlight java %}0x2D{% endihighlight %} in hexadecimal,
|
||||||
|
and {% ihighlight java %}445{% endihighlight %}
|
||||||
|
is {% ihighlight java %}0x1BD{% endihighlight %}. The color is simply placing in the last 6 bytes of the address, no
|
||||||
|
hexadecimal conversion needed.
|
||||||
|
|
||||||
|
By now, the basic concept should be clear. You can paint a pixel by sending a `ping` to a specific IPv6 address with the
|
||||||
|
arguments encoded into it.
|
||||||
|
|
||||||
|
In Python, the encoding can be done like so:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_ip(x: int, y: int, rgb: Tuple[int, int, int], large: bool = False):
|
||||||
|
return f"2a06:a003:d040:{'2' if large else '1'}{x:03X}:{y:03X}:{rgb[0]:02X}:{rgb[1]:02X}:{rgb[2]:02X}"
|
||||||
|
```
|
||||||
|
|
||||||
|
> I've been interested in ways to format this dynamically with arbitrary base IPv6 addresses, but I haven't found
|
||||||
|
> a performant way of calculating it. It seems like it would require lots of bit shifting, precalculated constants,
|
||||||
|
> and a C extension to maximize performance. A future endeavour, perhaps.
|
||||||
|
> Additionally, the base IP does not change often (I have only observed it changing once).
|
||||||
|
|
||||||
|
## The Rest of Place: IPv6
|
||||||
|
|
||||||
|
IPv6 place includes a proper canvas to boot, and you can view it at [v6.sys42.net][place-v6], along with the basic
|
||||||
|
instructions on how to use it.
|
||||||
|
|
||||||
|
The way you view (and technically, receive information), is through a WebSocket connection. In the browser, you'll
|
||||||
|
receive updates that are combined into a Javascript-based canvas element.
|
||||||
|
|
||||||
|
On the initial page load, you'll receive a one-time message containing a PNG image of the current canvas. After that,
|
||||||
|
you'll receive partial updates whenever updates occur. These updates are also PNG images, but they are transparent
|
||||||
|
except for the pixels that have changed.
|
||||||
|
|
||||||
|
The WebSocket also contains information about PPS, or Pixels Per Second. This is the rate at which the canvas is
|
||||||
|
updated globally by all users. PPS messages are drawn onto a historical graph displayed below the canvas.
|
||||||
|
|
||||||
|
## 30 Minute Implementation
|
||||||
|
|
||||||
|
I had a lab due the day I decided to implement this, so I only had 30 minutes to spare. I decided to use Python, as it
|
||||||
|
offers a lot of flexibility and is my go-to language for anything quick.
|
||||||
|
|
||||||
|
I realized quite early on that any normal ping operations would be far too slow, and looked into batch pinging
|
||||||
|
implementations.
|
||||||
|
|
||||||
|
I found the [`multiping`][pypi-multiping] package first, and stuck with it. As the name implies, it specializes in
|
||||||
|
sending multiple pings at once, and is quite fast for Python. It also supports IPv6 without complaint.
|
||||||
|
|
||||||
|
The great part of this package is that it allows you to set timeouts - this is key in performance, as we don't care
|
||||||
|
about the response, only that the packet was sent. This allows us to send a large number of pings at once, and not
|
||||||
|
have to wait for a response.
|
||||||
|
|
||||||
|
Here was my first implementation:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def upload_pixels(pixels: List[Pixel], chunk_size: int = None):
|
||||||
|
"""
|
||||||
|
Given a list of pixels, upload them with the given chunk size.
|
||||||
|
"""
|
||||||
|
ips = [get_ip(x, y, rgb) for x, y, rgb in pixels]
|
||||||
|
return upload(ips, chunk_size)
|
||||||
|
|
||||||
|
|
||||||
|
def upload(ips: List[str], chunk_size: int = None):
|
||||||
|
# Default to maximum chunk size
|
||||||
|
if chunk_size is None: chunk_size = maximum_chunk
|
||||||
|
|
||||||
|
chunked = list(chunkify(ips, min(maximum_chunk, chunk_size)))
|
||||||
|
for i, chunk in enumerate(chunked, start=1):
|
||||||
|
print(f'Chunk {i}/{len(chunked)}')
|
||||||
|
multi_ping(chunk, timeout=0.2, retry=0)
|
||||||
|
```
|
||||||
|
|
||||||
|
`multiping` only supports sending a maximum of 10,000 pings at once, so I chunked the list of IPs into groups of 10,000
|
||||||
|
before letting `multiping` handle the rest.
|
||||||
|
|
||||||
|
Receiving data from the Websocket required a bit more guesswork, but the ultimate implementation is quite
|
||||||
|
straightforward:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def get_image(websocket):
|
||||||
|
while True:
|
||||||
|
data = await websocket.recv()
|
||||||
|
if type(data) == bytes:
|
||||||
|
return Image.open(io.BytesIO(data))
|
||||||
|
```
|
||||||
|
|
||||||
|
I decided to use [Pillow][pypi-pillow] for image manipulation, as it's a well-known and well-supported library. I
|
||||||
|
went with [`websockets`][pypi-websockets] for the WebSocket implementation.
|
||||||
|
|
||||||
|
Since the Websocket is used for both image updates and PPS data, a type check is required. PPS data is completely
|
||||||
|
ignored.
|
||||||
|
|
||||||
|
## The Final Implementation
|
||||||
|
|
||||||
|
While my first implementation was just fine for 30 minutes, and I could have left off there, I wanted to see if I could
|
||||||
|
do better. Initial usage demonstrated that any given pixel could fail to be placed - at high PPS, it seems some pixels
|
||||||
|
don't reach the server. Either the packets are dropped, they aren't ever sent, or the server simply doesn't process
|
||||||
|
them.
|
||||||
|
|
||||||
|
Whatever the case, a 'repainting' mechanism had to be implemented, and the Canvas had to be kept in memory.
|
||||||
|
|
||||||
|
I manage this with an async-based `PlaceClient` class that provides methods for receiving WebSocket updates, storing
|
||||||
|
the canvas image, identifying differences between the canvas and the local image, and sending pixels synchronously.
|
||||||
|
|
||||||
|
Here's what my final API looks like:
|
||||||
|
|
||||||
|
```python
|
||||||
|
client = await PlaceClient.connect(os.getenv(Environment.WEBSOCKET_ADDRESS))
|
||||||
|
# Set the current target to the image we want on the canvas
|
||||||
|
client.current_target = original_image
|
||||||
|
# Launch a background task to receive updates from the WebSocket and update our local 'canvas'
|
||||||
|
asyncio.create_task(client.receive())
|
||||||
|
# Paint the image with up to 5% error (difference tolerance)
|
||||||
|
await client.complete(0.05)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can check out the repository at [github.com/Xevion/v6-place][github-v6-place].
|
||||||
|
|
||||||
|
[place-v6]: https://v6.sys42.net/
|
||||||
|
|
||||||
|
[place-reddit]: https://www.reddit.com/r/place/
|
||||||
|
|
||||||
|
[pypi-multiping]: https://pypi.org/project/multiping/
|
||||||
|
|
||||||
|
[pypi-pillow]: https://pypi.org/project/Pillow/
|
||||||
|
|
||||||
|
[pypi-websockets]: https://pypi.org/project/websockets/
|
||||||
|
|
||||||
|
[github-v6-place]: https://github.com/Xevion/v6-place
|
||||||
Reference in New Issue
Block a user