Merge pull request #60 from e1ven/2025-simple-updates

Update Robohash for modern Python, and to re-publish in Pypi
This commit is contained in:
Robohash 2025-03-15 16:35:01 -04:00 committed by GitHub
commit 71dec1296c
19 changed files with 628 additions and 213 deletions

8
.dockerignore Normal file
View File

@ -0,0 +1,8 @@
.git
.gitignore
__pycache__
*.pyc
dist
*.egg-info
.dockerignore
Dockerfile

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Track binary files for tests
tests/reference/*.png binary

52
.github/workflows/docker-publish.yml vendored Normal file
View File

@ -0,0 +1,52 @@
name: Build and Publish Docker Image
on:
push:
# Publish when tags are pushed
tags:
- '*'
env:
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Login to GitHub Container Registry
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata for Docker
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
# Build and push Docker image
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

44
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,44 @@
name: Robohash Tests
on:
push:
branches: [ master, main ]
pull_request:
branches: [ master, main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and run Docker container
run: |
docker build -t robohash:test .
docker run --name robohash-test -d robohash:test
# Wait for container to start
sleep 5
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pillow
- name: Install the package
run: |
pip install -e .
- name: Run consistency tests
run: |
python tests/test_image_consistency.py
- name: Clean up
run: |
docker stop robohash-test || true
docker rm robohash-test || true

33
.gitignore vendored
View File

@ -1,6 +1,33 @@
*.pyc
# Python bytecode
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
dist/
build/
*.egg-info/
*.egg
# Virtual environments
venv/
env/
ENV/
# Editor and OS artifacts
*.swp
*.swo
*~
.vscode/
.idea/
.DS_Store
*.Pluric*Settings*
# Runtime artifacts
*.log
nohup.out
__pycache__
*.pid
# Test artifacts
.coverage
htmlcov/
.pytest_cache/

34
Dockerfile Normal file
View File

@ -0,0 +1,34 @@
FROM debian:12-slim
# Set environment variables
ENV PORT=8080
ENV PYTHONUNBUFFERED=1
# Install system dependencies in a single layer to reduce image size
RUN apt-get update && apt-get install -y \
python3 \
python3-pip \
python3-pillow \
python3-tornado \
python3-natsort \
--no-install-recommends && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
# Copy the application code
COPY robohash/ /app/robohash/
COPY setup.py .
COPY README.md .
# Install the application in development mode
# Use the --break-system-packages flag since this is a controlled environment
RUN pip3 install -e . --break-system-packages
# Expose the port that the application will run on
EXPOSE 8080
# Command to run the application
CMD ["python3", "-m", "robohash.webfront"]

213
README.md Normal file
View File

@ -0,0 +1,213 @@
# RoboHash
The source code for [RoboHash.org](https://robohash.org/).
This tool generates unique images from a given string of text.
Put in any text, such as IP address, email, filename, userid, or whatever else you like, and get back a robot/alien/monster/cat/human image for your site. With hundreds of millions of variations, Robohash is among the leading robot-based hashing tools on the web.
It operates by algorithmically assembling various robot components together, using bits from the SHA hash. Not perfect, not entirely secure, but it gives a good gut-check to "Hey, this SHA is wrong."
# Ways to Use Robohash
There are several ways to use Robohash, depending on your needs:
## 1. Using the Public Service
The easiest way to use Robohash is through the public service at [robohash.org](https://robohash.org/).
Super Easy to use: Anytime you need a Robohash, just embed:
```html
<img src="https://robohash.org/YOUR-TEXT.png">
```
**URL Parameters and Advanced Options:**
- **Image Formats**: Want a JPG instead? Fine. PNG? Fine. Want it as a bitmap? We think you're nutty. But fine.
Just change the extension: `https://robohash.org/hash.jpg`
- **Size Control**: From destroying skyscrapers to nanobots, we've got you covered.
`https://robohash.org/hash?size=200x200`
- **Robot Sets**: Choose your preferred mechanical beings:
`https://robohash.org/hash?set=set2` (set1-set5 available, or "any")
- **Backgrounds**: Our robots like to travel. Add a background as part of the hash:
`https://robohash.org/hash?bgset=bg1` (bg1-bg2 available, or "any")
- **Gravatar Integration**: For Gravatar enthusiasts, you can ask Robohash to use a Gravatar if one is available:
`https://robohash.org/user@example.com?gravatar=yes`
or for pre-hashed emails: `https://robohash.org/hash?gravatar=hashed`
- **Directory Style Parameters**: We also accept commands via directory structure:
`https://robohash.org/set_set3/bgset_bg1/3.14159?size=500x500`
- **Extension Handling**: Want to hash the whole URL including extension? Use:
`https://robohash.org/hash.png?ignoreext=false`
**Important Notes:**
- Robohash.org is a best-effort service, not a commercial offering
- Our robots stay speedy due to caching modules and CDN usage
- If you receive errors or "too many requests" responses, please back off exponentially
- For high-volume or production use, consider hosting your own instance using Docker
- Very infrequent murderous rampages (that's a fact!)
## 2. Using Pre-built Docker Images
For the simplest self-hosted solution, use our pre-built Docker images:
```bash
# Pull the latest version
docker pull ghcr.io/e1ven/robohash:latest
# Run the container
docker run -p 8080:8080 ghcr.io/e1ven/robohash:latest
```
Your Robohash instance will be available at `http://localhost:8080`
## 3. Building Your Own Docker Image
If you want to customize the Docker image:
```bash
# Clone the repository
git clone https://github.com/e1ven/Robohash.git
cd Robohash
# Build the image
docker build -t robohash .
# Run the container
docker run -p 8080:8080 robohash
```
## 4. Python Library (Programmatic Usage)
For integration in Python applications, install the library:
```bash
pip install robohash
```
Then use it in your code:
```python
from robohash import Robohash
# Create a robot for any text
hash_text = "example@example.com"
rh = Robohash(hash_text)
# Customize your robot (all parameters optional)
rh.assemble(
roboset='set1', # Which set to use (set1-set5 or 'any')
color=None, # Color for set1 (only works with set1)
format='png', # Output format (png, jpg, etc)
bgset=None, # Background set (bg1, bg2, or None)
sizex=300, # Width
sizey=300 # Height
)
# Save to file
with open("robot.png", "wb") as f:
rh.img.save(f, format="png")
```
## 5. Web Frontend (Self-Hosted Service)
To run the web service on your own server:
```bash
# Install the package with web dependencies
pip install robohash[web]
# Run the web service (defaults to port 80)
python -m robohash.webfront
```
You can specify a different port:
```bash
PORT=8080 python -m robohash.webfront
```
## Development
Dependencies are managed using requirements.in and pip-compile:
```bash
$ pip install pip-tools
$ pip-compile requirements.in # Updates requirements.txt with pinned versions
$ pip install -r requirements.txt
```
## Usage
```python
from robohash import Robohash
hash = "whatever-hash-you-want"
rh = Robohash(hash)
rh.assemble(roboset='any')
with open("path/to/new/file.png", "wb") as f:
rh.img.save(f, format="png")
```
## Robosets
RoboHash comes with five distinct sets of mechanical/biological entities:
- **set1**: Classic robots created by Zikri Kader. The original robotic workforce.
- **set2**: Monsters created by Hrvoje Novakovic. A whole slew of random monsters.
- **set3**: Robot heads created by Julian Peter Arias. New, suave, disembodied heads. That's sexy. Like a robot.
- **set4**: Cats created by David Revoy. Hydroponically grown beautiful kittens.
- **set5**: Human avatars created by Pablo Stanley. For those afraid of the robocalypse, you can also generate human technicians to mind the robot army.
Specify which set you want in the `assemble()` method or through URL parameters. Alternatively, specify the string "any", and RoboHash will pick an image set for you, based on the provided hash.
If you use a specific set, or a list of them (like "?sets=1,3"), it'll probably stay the same as it is now. If you use "set=any", it'll include any new sets we happen to add, so existing hashes may change.
## License
The Python Code is available under the MIT/Expat license. See the `LICENSE.txt` file for the full text of this license. Copyright (c) 2011.
Feel free to embed the Robohash images, host your own instance of Robohash, or integrate them into your own project. If you do, please just mention where they came from :) Example wording might be "Robots lovingly delivered by Robohash.org" or similar.
### Image Sets Attribution
Robohash contains art from various talented creators:
- The "set1" artwork (and robohash backgrounds) were created by Zikri Kader. They are available under CC-BY-3.0 or CC-BY-4.0 license.
- The "set2" artwork was created by Hrvoje Novakovic. They are available under CC-BY-3.0 license.
- The "set3" artwork was created by Julian Peter Arias. They are available under CC-BY-3.0 license.
- The Cats/"set4" were created by David Revoy, used under CC-BY-4.0 https://www.peppercarrot.com/extras/html/2016_cat-generator/
- The avatars used in "set5" were created by Pablo Stanley, for https://avataaars.com/. They are "Free for personal and commercial use. 😇"
You are free to embed robots under the terms of the CC-BY license. Example wording might be "Robots lovingly delivered by Robohash.org" or something similarly respectful of our robotic overlords.
## Continuous Integration and Deployment
This project uses GitHub Actions for continuous integration and deployment:
1. When a tag is pushed (e.g., `v2.0.0`), a Docker image is automatically built and published to GitHub Container Registry
2. Images are tagged with both the specific version and `latest`
3. All container builds are publicly available
The automated workflow makes it easy to deploy new versions without manual building.
## Project Status
This project is considered mostly feature-complete and in maintenance mode. It's fun, and it does what it was designed to do - While I'm happy to fix critical bugs, I'm not actively developing new features or major enhancements.
The original project this was created for is no longer active, and Robohash continues to exist as a standalone service and library that accomplishes its core mission effectively.
Pull requests are welcome, but please understand that I may not be able to review or incorporate them in a timely manner, if at all. This isn't because I don't value your contributions, but simply reflects the reality of the project's lifecycle and my available time.
If you find Robohash useful, I encourage you to fork it and adapt it to your needs. The MIT license makes this easy, and I'm genuinely happy to see the project live on in other forms. Please remember that while the code is MIT licensed, the artwork in the various sets is under different Creative Commons licenses as detailed in the License section above. If you fork this project, be sure to respect these licenses and provide appropriate attribution.
## Original Disclaimer
OK, I'll admit I'm a crappy programmer. Compounding this, I wrote this code initially to be internal-only. It's ugly, and could be a LOT nicer.
Sorry about that.

View File

@ -1,85 +0,0 @@
RoboHash
========
The source code for `RoboHash.org`_.
It basically copy/pastes various robot pictures together, using bits
from the SHA hash. It's not perfect, and not entirely secure, but it
gives a good gut-check to "Hey, this SHA is wrong."
Install
-------
Just the library:
.. code:: bash
$ pip install robohash
Or if you also want the web frontend:
.. code:: bash
$ pip install robohash[web]
Usage
-----
.. code:: python
from robohash import Robohash
hash = "whatever-hash-you-want"
rh = Robohash(hash)
rh.assemble(roboset='any')
with open("path/to/new/file.png", "wb") as f:
rh.img.save(f, format="png")
Robosets
--------
RoboHash comes with five image sets, named "set1", "set2", "set3", "set4" and "set5".
Specify which set you want in the ``assemble()`` method. Alternatively,
specify the string "any", and RoboHash will pick an image set for you,
based on the provided hash.
License
-------
The Python Code is available under the MIT/Expat license. See the
``LICENSE.txt`` file for the full text of this license. Copyright (c)
2011.
Feel free to embed the Robohash images, host your own instance of Robohash,
or integrate them into your own project.
If you do, please just mention where they came from :)
Example wording might be "Robots lovingly delivered by Robohash.org" or similar.
The "set1" artwork (and robohash backgrounds) were created by Zikri Kader.
They are available under CC-BY-3.0 or CC-BY-4.0 license.
The "set2" artwork was created by Hrvoje Novakovic.
They are available under CC-BY-3.0 license.
The "set3" artwork was created by Julian Peter Arias.
They are available under CC-BY-3.0 license.
The Cats/"set4" were created by David Revoy, used under CC-BY-4.0
https://www.peppercarrot.com/extras/html/2016_cat-generator/
The avatars used in "set5" were created by Pablo Stanley, for https://avataaars.com/
They are "Free for personal and commercial use. 😇"
Disclaimer
----------
OK, I'll admit I'm a crappy programmer. Compounding this, I wrote this
code initially to be internal-only. It's ugly, and could be a LOT nicer.
Sorry about that.
.. _RoboHash.org: https://robohash.org/

3
requirements.in Normal file
View File

@ -0,0 +1,3 @@
pillow>=9.1.1
tornado>=6.1
natsort>=8.1.0

View File

@ -1,3 +1,7 @@
pillow
tornado
natsort
# Generated using pip-compile
# To regenerate, run:
# pip-compile requirements.in
#
pillow==9.1.1
tornado==6.1
natsort==8.1.0

View File

@ -1 +1,4 @@
from .robohash import Robohash
from .robohash import Robohash
__version__ = '2.0a1'
__all__ = ['Robohash']

View File

@ -1,26 +1,37 @@
import argparse
import io
import sys
from typing import NoReturn
from robohash import Robohash
def main():
parser = argparse.ArgumentParser()
parser.add_argument("-s", "--set", default="set1")
parser.add_argument("-x", "--width", type=int, default=300)
parser.add_argument("-y", "--height", type=int, default=300)
parser.add_argument("-f", "--format", default="png")
parser.add_argument("-b", "--bgset")
parser.add_argument("-o", "--output", default="robohash.png")
parser.add_argument("text", help="Text to use for the hash")
args = parser.parse_args()
robohash = Robohash(args.text)
robohash.assemble(roboset=args.set, bgset=args.bgset,
sizex=args.width, sizey=args.height)
robohash.img.save(args.output, format=args.format)
def main() -> NoReturn:
"""
Command-line interface for Robohash.
Parses arguments and generates a robot image based on input text.
"""
parser = argparse.ArgumentParser(description="Generate a robot hash image from text input")
parser.add_argument("-s", "--set", default="set1", help="Robot set to use (set1, set2, set3, set4, set5, or 'any')")
parser.add_argument("-x", "--width", type=int, default=300, help="Width of output image")
parser.add_argument("-y", "--height", type=int, default=300, help="Height of output image")
parser.add_argument("-f", "--format", default="png", help="Output format (png, jpeg, etc.)")
parser.add_argument("-b", "--bgset", help="Background set to use (bg1, bg2, or 'any')")
parser.add_argument("-o", "--output", default="robohash.png", help="Output filename")
parser.add_argument("text", help="Text to use for the hash")
args = parser.parse_args()
robohash = Robohash(args.text)
robohash.assemble(
roboset=args.set,
bgset=args.bgset,
sizex=args.width,
sizey=args.height,
format=args.format
)
robohash.img.save(args.output, format=args.format)
sys.exit(0)
if __name__ == "__main__":
main()

View File

@ -1,19 +1,24 @@
# This Python file uses the following encoding: utf-8
import os
import hashlib
from typing import List, Optional, Union, Any
from PIL import Image
import natsort
class Robohash(object):
class Robohash:
"""
Robohash is a quick way of generating unique avatars for a site.
The original use-case was to create somewhat memorable images to represent a RSA key.
"""
def __init__(self,string,hashcount=11,ignoreext = True):
def __init__(self, string: str, hashcount: int = 11, ignoreext: bool = True):
"""
Creates our Robohasher
Takes in the string to make a Robohash out of.
Args:
string: The input string to hash
hashcount: Number of hash segments to create
ignoreext: Whether to ignore file extensions in the string
"""
# Default to png
@ -25,29 +30,35 @@ class Robohash(object):
string = string.encode('utf-8')
hash = hashlib.sha512()
hash.update(string)
self.hexdigest = hash.hexdigest()
self.hasharray = []
#Start this at 4, so earlier is reserved
#0 = Color
#1 = Set
#2 = bgset
#3 = BG
hash_obj = hashlib.sha512()
hash_obj.update(string)
self.hexdigest = hash_obj.hexdigest()
self.hasharray: List[int] = []
# Start this at 4, so earlier is reserved
# 0 = Color
# 1 = Set
# 2 = bgset
# 3 = BG
self.iter = 4
self._create_hashes(hashcount)
self.resourcedir = os.path.dirname(__file__) + '/'
self.resourcedir = f"{os.path.dirname(__file__)}/"
# Get the list of backgrounds and RobotSets
self.sets = self._listdirs(self.resourcedir + 'sets')
self.bgsets = self._listdirs(self.resourcedir + 'backgrounds')
self.sets = self._listdirs(f"{self.resourcedir}sets")
self.bgsets = self._listdirs(f"{self.resourcedir}backgrounds")
# Get the colors in set1
self.colors = self._listdirs(self.resourcedir + 'sets/set1')
self.colors = self._listdirs(f"{self.resourcedir}sets/set1")
def _remove_exts(self,string):
def _remove_exts(self, string: str) -> str:
"""
Sets the string, to create the Robohash
Args:
string: Input string that may contain an image extension
Returns:
string with extension removed if present
"""
# If the user hasn't disabled it, we will detect image extensions, such as .png, .jpg, etc.
@ -55,25 +66,28 @@ class Robohash(object):
# This ensures that /Bear.png and /Bear.bmp will send back the same image, in different formats.
if string.lower().endswith(('.png','.gif','.jpg','.bmp','.jpeg','.ppm','.datauri')):
format = string[string.rfind('.') +1 :len(string)]
if format.lower() == 'jpg':
format = 'jpeg'
self.format = format
string = string[0:string.rfind('.')]
format_str = string[string.rfind('.') + 1:]
if format_str.lower() == 'jpg':
format_str = 'jpeg'
self.format = format_str
string = string[:string.rfind('.')]
return string
def _create_hashes(self,count):
def _create_hashes(self, count: int) -> None:
"""
Breaks up our hash into slots, so we can pull them out later.
Essentially, it splits our SHA/MD5/etc into X parts.
Args:
count: Number of segments to split the hash into
"""
for i in range(0,count):
#Get 1/numblocks of the hash
for i in range(count):
# Get 1/numblocks of the hash
blocksize = int(len(self.hexdigest) / count)
currentstart = (1 + i) * blocksize - blocksize
currentend = (1 +i) * blocksize
self.hasharray.append(int(self.hexdigest[currentstart:currentend],16))
currentend = (1 + i) * blocksize
self.hasharray.append(int(self.hexdigest[currentstart:currentend], 16))
# Workaround for adding more sets in 2019.
# We run out of blocks, because we use some for each set, whether it's called or not.
@ -81,13 +95,28 @@ class Robohash(object):
# This shouldn't reduce the security since it should only draw from one set of these in practice.
self.hasharray = self.hasharray + self.hasharray
def _listdirs(self,path):
def _listdirs(self, path: str) -> List[str]:
"""
Get a list of directories at the given path
Args:
path: Path to search for directories
Returns:
List of directory names (not full paths)
"""
return [d for d in natsort.natsorted(os.listdir(path)) if os.path.isdir(os.path.join(path, d))]
def _get_list_of_files(self,path):
def _get_list_of_files(self, path: str) -> List[str]:
"""
Go through each subdirectory of `path`, and choose one file from each to use in our hash.
Continue to increase self.iter, so we use a different 'slot' of randomness each time.
Args:
path: Root directory to search for image files
Returns:
List of chosen file paths, one from each subdirectory
"""
chosen_files = []
@ -95,7 +124,7 @@ class Robohash(object):
directories = []
for root, dirs, files in natsort.natsorted(os.walk(path, topdown=False)):
for name in dirs:
if name[:1] != '.':
if not name.startswith('.'):
directories.append(os.path.join(root, name))
directories = natsort.natsorted(directories)
@ -104,7 +133,7 @@ class Robohash(object):
for directory in directories:
files_in_dir = []
for imagefile in natsort.natsorted(os.listdir(directory)):
files_in_dir.append(os.path.join(directory,imagefile))
files_in_dir.append(os.path.join(directory, imagefile))
files_in_dir = natsort.natsorted(files_in_dir)
# Use some of our hash bits to choose which file
@ -114,39 +143,54 @@ class Robohash(object):
return chosen_files
def assemble(self,roboset=None,color=None,format=None,bgset=None,sizex=300,sizey=300):
def assemble(self,
roboset: Optional[str] = None,
color: Optional[str] = None,
format: Optional[str] = None,
bgset: Optional[str] = None,
sizex: int = 300,
sizey: int = 300) -> None:
"""
Build our Robot!
Returns the robot image itself.
Returns the robot image itself in self.img.
Args:
roboset: Which robot set to use ('set1', 'set2', etc. or 'any')
color: Color to use (only works with set1)
format: Image format ('png', 'jpeg', etc.)
bgset: Background set to use
sizex: Width of the final image
sizey: Height of the final image
"""
# Allow users to manually specify a robot 'set' that they like.
# Ensure that this is one of the allowed choices, or allow all
# If they don't set one, take the first entry from sets above.
if roboset == 'any':
roboset = self.sets[self.hasharray[1] % len(self.sets) ]
roboset = self.sets[self.hasharray[1] % len(self.sets)]
elif roboset in self.sets:
roboset = roboset
# No need to reassign roboset to itself
pass
else:
roboset = self.sets[0]
# Only set1 is setup to be color-seletable. The others don't have enough pieces in various colors.
# Only set1 is setup to be color-selectable. The others don't have enough pieces in various colors.
# This could/should probably be expanded at some point..
# Right now, this feature is almost never used. ( It was < 44 requests this year, out of 78M reqs )
# Right now, this feature is almost never used. (It was < 44 requests this year, out of 78M reqs)
if roboset == 'set1':
if color in self.colors:
roboset = 'set1/' + color
roboset = f"set1/{color}"
else:
randomcolor = self.colors[self.hasharray[0] % len(self.colors) ]
roboset = 'set1/' + randomcolor
randomcolor = self.colors[self.hasharray[0] % len(self.colors)]
roboset = f"set1/{randomcolor}"
# If they specified a background, ensure it's legal, then give it to them.
if bgset in self.bgsets:
bgset = bgset
# No need to reassign bgset to itself
pass
elif bgset == 'any':
bgset = self.bgsets[ self.hasharray[2] % len(self.bgsets) ]
bgset = self.bgsets[self.hasharray[2] % len(self.bgsets)]
# If we set a format based on extension earlier, use that. Otherwise, PNG.
if format is None:
@ -161,40 +205,41 @@ class Robohash(object):
# For instance, the head has to go down BEFORE the eyes, or the eyes would be hidden.
# First, we'll get a list of parts of our robot.
roboparts = self._get_list_of_files(self.resourcedir + 'sets/' + roboset)
roboparts = self._get_list_of_files(f"{self.resourcedir}sets/{roboset}")
# Now that we've sorted them by the first number, we need to sort each sub-category by the second.
roboparts.sort(key=lambda x: x.split("#")[1])
background = None
if bgset is not None:
bglist = []
backgrounds = natsort.natsorted(os.listdir(self.resourcedir + 'backgrounds/' + bgset))
backgrounds.sort()
for ls in backgrounds:
if not ls.startswith("."):
bglist.append(self.resourcedir + 'backgrounds/' + bgset + "/" + ls)
background = bglist[self.hasharray[3] % len(bglist)]
bglist = []
backgrounds = natsort.natsorted(os.listdir(f"{self.resourcedir}backgrounds/{bgset}"))
backgrounds.sort()
for ls in backgrounds:
if not ls.startswith("."):
bglist.append(f"{self.resourcedir}backgrounds/{bgset}/{ls}")
background = bglist[self.hasharray[3] % len(bglist)]
# Paste in each piece of the Robot.
roboimg = Image.open(roboparts[0])
roboimg = roboimg.resize((1024,1024))
roboimg = roboimg.resize((1024, 1024))
for png in roboparts:
img = Image.open(png)
img = img.resize((1024,1024))
roboimg.paste(img,(0,0),img)
img = Image.open(png)
img = img.resize((1024, 1024))
roboimg.paste(img, (0, 0), img)
if bgset is not None:
bg = Image.open(background)
bg = bg.resize((1024,1024))
bg.paste(roboimg,(0,0),roboimg)
roboimg = bg
if bgset is not None and background is not None:
bg = Image.open(background)
bg = bg.resize((1024, 1024))
bg.paste(roboimg, (0, 0), roboimg)
roboimg = bg
# If we're a BMP, flatten the image.
if format in ['bmp','jpeg']:
#Flatten bmps
r, g, b, a = roboimg.split()
roboimg = Image.merge("RGB", (r, g, b))
if format in ['bmp', 'jpeg']:
# Flatten bmps
r, g, b, a = roboimg.split()
roboimg = Image.merge("RGB", (r, g, b))
self.img = roboimg.resize((sizex,sizey),Image.LANCZOS)
self.img = roboimg.resize((sizex, sizey), Image.LANCZOS)
self.format = format

View File

@ -7,5 +7,17 @@ Allow: Liquid-metal-form
User-agent: WALL-E
Allow: Rebuilding-with-EVE
User-agent: GPTBot
Allow: *,DoYouReallyNeedMORERobots
User-agent: anthropic-ai
Allow: *,ArentRobotsAnthrocentricEnough?
User-agent: FacebookBot
Allow: *,ZuckWantsToBeRealBoy
User-agent: Bingbot
Allow: *,R2D2WasTheBetterCopilot
User-agent: *
Allow: /

View File

@ -1,9 +1,6 @@
#!/usr/bin/env python
# This Python file uses the following encoding: utf-8
#!/usr/bin/env python3
# Find details about this project at https://github.com/e1ven/robohash
from __future__ import unicode_literals
import tornado.httpserver
import tornado.ioloop
import tornado.options
@ -17,22 +14,15 @@ import re
import io
import base64
# Import urllib stuff that works in both Py2 and Py3
try:
import urllib.request
import urllib.parse
urlopen = urllib.request.urlopen
urlencode = urllib.parse.urlencode
except ImportError:
import urllib2
import urllib
urlopen = urllib2.urlopen
urlencode = urllib.urlencode
import urllib.request
import urllib.parse
urlopen = urllib.request.urlopen
urlencode = urllib.parse.urlencode
from tornado.options import define, options
import io
define("port", default=80, help="run on the given port", type=int)
define("port", default=int(os.environ.get("PORT", 80)), help="run on the given port", type=int)
@ -233,14 +223,19 @@ class ImgHandler(tornado.web.RequestHandler):
The ImageHandler is our tornado class for creating a robot.
called as Robohash.org/$1, where $1 becomes the seed string for the Robohash obj
"""
def get(self,string=None):
def get(self, string: str = None):
"""
Handle GET requests for robot images
Args:
string: Input string to hash into a robot
"""
# Set default values
sizex = 300
sizey = 300
format = "png"
bgset = None
color = None
sizex: int = 300
sizey: int = 300
format: str = "png"
bgset: str = None
color: str = None
# Normally, we pass in arguments with standard HTTP GET variables, such as
# ?set=any and &size=100x100
@ -275,7 +270,7 @@ class ImgHandler(tornado.web.RequestHandler):
# Ensure we have something to hash!
if string is None:
string = self.request.remote_ip
string = self.request.remote_ip
# Detect if the user has passed in a flag to ignore extensions.
@ -297,12 +292,12 @@ class ImgHandler(tornado.web.RequestHandler):
if args.get('gravatar','').lower() == 'yes':
# They have requested that we hash the email, and send it to Gravatar.
default = "404"
gravatar_url = "https://secure.gravatar.com/avatar/" + hashlib.md5(string.lower().encode('utf-8')).hexdigest() + "?"
gravatar_url = f"https://secure.gravatar.com/avatar/{hashlib.md5(string.lower().encode('utf-8')).hexdigest()}?"
gravatar_url += urlencode({'default':default, 'size':str(sizey)})
elif args.get('gravatar','').lower() == 'hashed':
# They have sent us a pre-hashed email address.
default = "404"
gravatar_url = "https://secure.gravatar.com/avatar/" + string + "?"
gravatar_url = f"https://secure.gravatar.com/avatar/{string}?"
gravatar_url += urlencode({'default':default, 'size':str(sizey)})
# If we do want a gravatar, request one. If we can't get it, just keep going, and return a robohash
@ -387,9 +382,7 @@ def main():
settings = {
"static_path": os.path.join(os.path.dirname(__file__), "static"),
"cookie_secret": "9b90a85cfe46cad5ec136ee44a3fa332",
"login_url": "/login",
"xsrf_cookies": True,
# No need for authentication or XSRF protection for an image service
}
application = tornado.web.Application([
@ -404,7 +397,7 @@ def main():
http_server = tornado.httpserver.HTTPServer(application,xheaders=True)
http_server.listen(options.port)
print("The Oven is warmed up - Time to make some Robots! Listening on port: " + str(options.port))
print(f"The Oven is warmed up - Time to make some Robots! Listening on port: {options.port}")
tornado.ioloop.IOLoop.instance().start()
if __name__ == "__main__":
main()

View File

@ -3,25 +3,31 @@ try:
except ImportError:
from distutils.core import setup
with open('README.rst', encoding='utf-8') as file:
with open('README.md', encoding='utf-8') as file:
long_description = file.read()
setup(
name='robohash',
packages=['robohash'],
version='1.0',
version='2.0a1',
description='One of the leading robot-based hashing tools on the web',
long_description=long_description,
long_description_content_type='text/markdown',
author='e1ven',
author_email='robo@robohash.org',
url='https://github.com/e1ven/Robohash',
download_url='https://github.com/e1ven/Robohash/tarball/1.0',
keywords=['robots'], # arbitrary keywords
download_url='https://github.com/e1ven/Robohash/tarball/2.0a1',
keywords=['robots', 'avatar', 'identicon'],
license='MIT',
classifiers=[
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Development Status :: 4 - Beta",
"Topic :: Security",
],
package_data={
@ -34,8 +40,9 @@ setup(
'backgrounds/*/*',
]
},
install_requires=['pillow', 'natsort'],
install_requires=['pillow>=9.1.1', 'natsort>=8.1.0'],
extras_require={
'web': ['tornado'],
'web': ['tornado>=6.1'],
},
python_requires='>=3.6',
)

1
tests/__init__.py Normal file
View File

@ -0,0 +1 @@
# Tests for Robohash

BIN
tests/reference/pi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""
Test to ensure consistency of Robohash image generation.
This test checks that the output of the Robohash generator matches
expected reference images for specific inputs.
"""
import os
import unittest
from PIL import Image, ImageChops
import io
from robohash import Robohash
class TestRobohashConsistency(unittest.TestCase):
def setUp(self):
self.reference_dir = os.path.join(os.path.dirname(__file__), 'reference')
def test_pi_image_consistency(self):
"""Test that '3.14159' generates the expected image."""
# Load the reference image
reference_path = os.path.join(self.reference_dir, 'pi.png')
reference_img = Image.open(reference_path)
# Generate a new image using the same input
rh = Robohash("3.14159")
rh.assemble(sizex=300, sizey=300)
# Convert the generated image to bytes for comparison
img_buffer = io.BytesIO()
rh.img.save(img_buffer, format="PNG")
img_buffer.seek(0)
generated_img = Image.open(img_buffer)
# Compare the images
diff = ImageChops.difference(reference_img.convert('RGBA'), generated_img.convert('RGBA'))
# If the images are the same, the difference will be all black (0)
self.assertFalse(diff.getbbox(), "Generated image doesn't match the reference image")
if __name__ == '__main__':
unittest.main()