mirror of
https://github.com/e1ven/Robohash.git
synced 2025-06-23 21:35:02 +00:00
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:
commit
71dec1296c
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@ -0,0 +1,8 @@
|
||||
.git
|
||||
.gitignore
|
||||
__pycache__
|
||||
*.pyc
|
||||
dist
|
||||
*.egg-info
|
||||
.dockerignore
|
||||
Dockerfile
|
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Track binary files for tests
|
||||
tests/reference/*.png binary
|
52
.github/workflows/docker-publish.yml
vendored
Normal file
52
.github/workflows/docker-publish.yml
vendored
Normal 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
44
.github/workflows/test.yml
vendored
Normal 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
33
.gitignore
vendored
@ -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
34
Dockerfile
Normal 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
213
README.md
Normal 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.
|
85
README.rst
85
README.rst
@ -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
3
requirements.in
Normal file
@ -0,0 +1,3 @@
|
||||
pillow>=9.1.1
|
||||
tornado>=6.1
|
||||
natsort>=8.1.0
|
@ -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
|
||||
|
@ -1 +1,4 @@
|
||||
from .robohash import Robohash
|
||||
from .robohash import Robohash
|
||||
|
||||
__version__ = '2.0a1'
|
||||
__all__ = ['Robohash']
|
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
@ -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: /
|
||||
|
@ -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()
|
||||
|
21
setup.py
21
setup.py
@ -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
1
tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Tests for Robohash
|
BIN
tests/reference/pi.png
Normal file
BIN
tests/reference/pi.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
41
tests/test_image_consistency.py
Normal file
41
tests/test_image_consistency.py
Normal 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()
|
Loading…
x
Reference in New Issue
Block a user