Update Exploring Near Earth Objects project and add Meme Generator project

This commit is contained in:
2026-01-03 21:55:24 -08:00
parent 9a4c3f7854
commit 155f0c9c6d
36 changed files with 754 additions and 65 deletions

View File

@@ -0,0 +1,35 @@
"""Meme engine module for handling the generation of meme images."""
import random
from PIL import Image, ImageFont, ImageDraw
class MemeEngine:
"""Class to generate memes from images and text."""
def __init__(self, output_path):
"""Initialize meme engine with path to save generated memes."""
self.output_path = output_path
def make_meme(self, img_path, text, author, width=500) -> str:
"""Generate a meme image with given text and author."""
output_file = f"{self.output_path}/{random.randint(0,10000)}.jpg"
message = f"{text}\n- {author}"
try:
with Image.open(img_path) as img:
# Resize image while maintaining aspect ratio
width = 500 if img.size[0] > 500 else img.size[0]
ratio = width / (img.size[0] * 1.0)
height = ratio * img.size[1]
img = img.resize((int(width), int(height)), Image.NEAREST)
draw = ImageDraw.Draw(img)
font = ImageFont.truetype(
"./_data/font/calibri_regular.ttf", int(height / 20)
)
draw.text((20, 20), message, font=font, fill="white")
img.save(output_file)
except Exception as err:
print(f"Error: {err}")
return output_file

View File

@@ -0,0 +1 @@
from .MemeEngine import MemeEngine

View File

@@ -0,0 +1,24 @@
"""Module for ingesting CSV files containing quotes."""
from typing import List
from .IngestorInterface import IngestorInterface
from .QuoteModel import QuoteModel
import pandas as pd
class CSVIngestor(IngestorInterface):
"""Subclass for ingesting CSV files."""
allowed_extensions = ["csv"]
@classmethod
def parse(cls, path: str) -> List[QuoteModel]:
"""Parse the CSV file to extract quotes."""
if not cls.can_ingest(path):
raise Exception("Invalid ingest path")
quotes = []
df = pd.read_csv(path, header=0, sep=",", names=["body", "author"])
for _, row in df.iterrows():
quotes.append(QuoteModel(row["body"], row["author"]))
return quotes

View File

@@ -0,0 +1,25 @@
"""Module for ingesting Docx files containing quotes."""
import docx
from typing import List
from .IngestorInterface import IngestorInterface
from .QuoteModel import QuoteModel
class DocxIngestor(IngestorInterface):
"""Subclass for ingesting Docx files."""
allowed_extensions = ["docx"]
@classmethod
def parse(cls, path: str) -> List[QuoteModel]:
"""Parse the Docx file to extract quotes."""
if not cls.can_ingest(path):
raise Exception("Invalid ingest path")
quotes = []
doc = docx.Document(path)
for para in doc.paragraphs:
if para.text != "":
parts = para.text.split(" - ")
quotes.append(QuoteModel(parts[0], parts[1]))
return quotes

View File

@@ -0,0 +1,22 @@
"""Ingestor module to select appropriate ingestor based on file type."""
from typing import List
from .IngestorInterface import IngestorInterface
from .QuoteModel import QuoteModel
from .CSVIngestor import CSVIngestor
from .TextIngestor import TextIngestor
from .DocxIngestor import DocxIngestor
from .PDFIngestor import PDFIngestor
class Ingestor(IngestorInterface):
"""Subclass to select appropriate ingestor."""
ingestors = [CSVIngestor, TextIngestor, DocxIngestor, PDFIngestor]
@classmethod
def parse(cls, path: str) -> List[QuoteModel]:
"""Select the appropriate ingestor to parse the file."""
for ingestor in cls.ingestors:
if ingestor.can_ingest(path):
return ingestor.parse(path)

View File

@@ -0,0 +1,23 @@
"""Ingestor Interface module for quote ingestion."""
from abc import ABC, abstractmethod
from typing import List
from .QuoteModel import QuoteModel
class IngestorInterface(ABC):
"""Base Ingestor Interface."""
allowed_extensions = []
@classmethod
def can_ingest(cls, path: str) -> bool:
"""Check if the ingestor can ingest the file based on its extension."""
ext = path.split(".")[-1]
return ext in cls.allowed_extensions
@classmethod
@abstractmethod
def parse(cls, path: str) -> List[QuoteModel]:
"""Abstract method to parse the file and return a list of QuoteModel objects."""
pass

View File

@@ -0,0 +1,40 @@
"""Module for ingesting PDF files containing quotes."""
import os
import random
import subprocess
from typing import List
from .IngestorInterface import IngestorInterface
from .QuoteModel import QuoteModel
class PDFIngestor(IngestorInterface):
"""Subclass for ingesting PDF files."""
allowed_extensions = ["pdf"]
@classmethod
def parse(cls, path: str) -> List[QuoteModel]:
"""Parse the PDF file to extract quotes."""
if not cls.can_ingest(path):
raise Exception("Invalid ingest path")
quotes = []
tmp = f"./tmp/{random.randint(0, 10000)}.txt"
try:
# pdftotext <input-pdf> <output-text-file>
call = subprocess.call(["pdftotext", path, tmp])
with open(tmp, "r") as file:
lines = file.readlines()
except FileNotFoundError as err:
print(f"Error: {err}")
else:
for line in lines:
line = line.strip()
if line:
parts = line.split(" - ")
quotes.append(QuoteModel(parts[0], parts[1]))
finally:
if os.path.exists(tmp):
os.remove(tmp)
return quotes

View File

@@ -0,0 +1,14 @@
"""QuoteModel module for representing a quote with its body and author."""
class QuoteModel:
"""Quote model class."""
def __init__(self, body, author):
"""Initialize the QuoteModel object."""
self.body = body
self.author = author
def __repr__(self):
"""String representation of the QuoteModel object."""
return f"{self.body} - {self.author}"

View File

@@ -0,0 +1,26 @@
"""Module for ingesting text files containing quotes."""
from typing import List
from .IngestorInterface import IngestorInterface
from .QuoteModel import QuoteModel
class TextIngestor(IngestorInterface):
"""Subcalss for ingesting text files."""
allowed_extensions = ["txt"]
@classmethod
def parse(cls, path: str) -> List[QuoteModel]:
"""Parse the text file to extract quotes."""
if not cls.can_ingest(path):
raise Exception("Invalid ingest path")
quotes = []
with open(path, "r") as file:
for line in file.readlines():
line = line.strip()
if line:
parts = line.split(" - ", 1)
if len(parts) == 2:
quotes.append(QuoteModel(parts[0], parts[1]))
return quotes

View File

@@ -0,0 +1,7 @@
from .IngestorInterface import IngestorInterface
from .CSVIngestor import CSVIngestor
from .DocxIngestor import DocxIngestor
from .PDFIngestor import PDFIngestor
from .TextIngestor import TextIngestor
from .Ingestor import Ingestor
from .QuoteModel import QuoteModel

39
Meme_Generator/README.md Normal file
View File

@@ -0,0 +1,39 @@
## Meme Generator Project
### Overview
The goal of this project is to build a "meme generator" a multimedia application to dynamically generate memes, including an image with an overlaid quote.
### Set up environment
```sh
$ sudo apt-get install -y xpdf
$ pip install -r requirements.txt
```
### Run program
Python test \
Usage: `python3 meme.py --path [image_path] --body [quote_body] --author [quote_author]`
```sh
$ mkdir tmp
$ python3 meme.py
```
Flask test
```sh
$ mkdir static
$ python3 app.py
```
Open link `http://127.0.0.1:5000` in web browser
### Primary modules
`QuoteEngine`: Meme engine module for handling the generation of meme images.\
`MemeEngine`: QuoteModel module for representing a quote with its body and author.
### Sub modules
`CSVIngestor`: Module for ingesting CSV files containing quotes. \
`DocxIngestor`: Module for ingesting Docx files containing quotes. \
`TextIngestor`: Module for ingesting text files containing quotes. \
`PDFIngestor`: Module for ingesting PDF files containing quotes.

View File

@@ -0,0 +1,3 @@
body,author
Chase the mailman,Skittle
"When in doubt, go shoe-shopping",Mr. Paws
1 body author
2 Chase the mailman Skittle
3 When in doubt, go shoe-shopping Mr. Paws

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,2 @@
To bork or not to bork - Bork
He who smelt it... - Stinky

View File

@@ -0,0 +1,6 @@
body,author
Line 1,Author 1
Line 2,Author 2
Line 3,Author 3
Line 4,Author 4
Line 5,Author 5
1 body author
2 Line 1 Author 1
3 Line 2 Author 2
4 Line 3 Author 3
5 Line 4 Author 4
6 Line 5 Author 5

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,7 @@
"Line 1" - Author 1
"Line 2" - Author 2
"Line 3" - Author 3
"Line 4" - Author 4
"Line 5" - Author 5

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

112
Meme_Generator/app.py Normal file
View File

@@ -0,0 +1,112 @@
import random
import os
import requests
from flask import Flask, render_template, abort, request
from QuoteEngine import Ingestor
from MemeEngine import MemeEngine
# Create the Flask application object. "__name__" tells Flask where to find templates and static files.
app = Flask(__name__)
# Create a global MemeEngine instance that will write generated memes into "./static"
# so that the images can be served by the web server.
meme = MemeEngine("./static")
def setup():
"""Load all resources"""
quote_files = [
"./_data/DogQuotes/DogQuotesTXT.txt",
"./_data/DogQuotes/DogQuotesDOCX.docx",
"./_data/DogQuotes/DogQuotesPDF.pdf",
"./_data/DogQuotes/DogQuotesCSV.csv",
]
quotes = []
for file in quote_files:
if Ingestor.parse(file) is not None:
quotes.append(Ingestor.parse(file))
images_path = "./_data/photos/dog/"
imgs = []
for root, _, files in os.walk(images_path):
imgs = [os.path.join(root, name) for name in files]
return quotes, imgs
# Call setup() once at import time to preload quotes and images.
# These will be reused on every request.
quotes, imgs = setup()
@app.route("/")
def meme_rand():
"""Generate a random meme
Steps:
- Pick a random image from imgs
- Pick a random "quote list" from quotes
- Pick a random quote from that list
- Generate a meme image with that quote and image
- Render a template to show the meme
"""
img = random.choice(imgs)
quote_list = random.choice(quotes)
quote = random.choice(quote_list)
path = meme.make_meme(img, quote.body, quote.author)
return render_template("meme.html", path=path)
@app.route("/create", methods=["GET"])
def meme_form():
"""User input for meme information
This route renders a form where the user can input:
- image_url: URL of the source image
- body: quote text
- author: quote author
"""
return render_template("meme_form.html")
@app.route("/create", methods=["POST"])
def meme_post():
"""Create a user defined meme
This route:
- Reads form data sent via POST from the meme_form page
- Downloads the image from the provided URL
- Saves it to a temporary file
- Passes that file to MemeEngine to generate a meme
- Deletes the temporary file
- Renders the final meme
"""
image_url = request.form.get("image_url", "").strip()
body = request.form.get("body", "").strip()
author = request.form.get("author", "").strip()
image = requests.get(image_url, timeout=5)
try:
tmp_file = f"tmp/{random.randint(0, 10000)}.jpg"
with open(tmp_file, "wb") as file:
file.write(image.content)
except:
print("Failed to generate meme")
path = None
if os.path.exists(tmp_file):
os.remove(tmp_file)
else:
path = meme.make_meme(tmp_file, body, author)
if os.path.exists(tmp_file):
os.remove(tmp_file)
finally:
return render_template("meme.html", path=path)
if __name__ == "__main__":
app.run()

54
Meme_Generator/meme.py Normal file
View File

@@ -0,0 +1,54 @@
import os
import random
import argparse
from MemeEngine import MemeEngine
from QuoteEngine import QuoteModel, Ingestor
def generate_meme(path=None, body=None, author=None):
"""Generate a meme given an path and a quote"""
img = None
quote = None
if path is None:
images = "./_data/photos/dog/"
imgs = []
for root, dirs, files in os.walk(images):
imgs = [os.path.join(root, name) for name in files]
img = random.choice(imgs)
else:
img = path[0]
if body is None:
quote_files = [
"./_data/DogQuotes/DogQuotesTXT.txt",
"./_data/DogQuotes/DogQuotesDOCX.docx",
"./_data/DogQuotes/DogQuotesPDF.pdf",
"./_data/DogQuotes/DogQuotesCSV.csv",
]
quotes = []
for f in quote_files:
quotes.extend(Ingestor.parse(f))
quote = random.choice(quotes)
else:
if author is None:
raise Exception("Author Required if Body is Used")
quote = QuoteModel(body, author)
meme = MemeEngine("./tmp")
path = meme.make_meme(img, quote.body, quote.author)
return path
if __name__ == "__main__":
"""Add path, quote, and author arguments for CLI, then print meme generation."""
parser = argparse.ArgumentParser(description="Meme Generator")
parser.add_argument("--path", type=str, help="Image path")
parser.add_argument("--body", type=str, help="Quote adding to meme")
parser.add_argument("--author", type=str, help="Author adding to meme")
args = parser.parse_args()
print(generate_meme(args.path, args.body, args.author))

View File

@@ -0,0 +1,29 @@
black==25.12.0
blinker==1.9.0
certifi==2025.11.12
charset-normalizer==3.4.4
click==8.3.1
Flask==3.1.2
idna==3.11
itsdangerous==2.2.0
Jinja2==3.1.6
lxml==6.0.2
MarkupSafe==3.0.3
mypy_extensions==1.1.0
numpy==2.2.6
packaging==25.0
pandas==2.3.3
pathspec==0.12.1
pillow==12.1.0
platformdirs==4.5.1
python-dateutil==2.9.0.post0
python-docx==1.2.0
pytokens==0.3.0
pytz==2025.2
requests==2.32.5
six==1.17.0
tomli==2.3.0
typing_extensions==4.15.0
tzdata==2025.3
urllib3==2.6.2
Werkzeug==3.1.4

View File

@@ -0,0 +1,37 @@
<!doctype html>
<html>
<head>
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<style>
body {
display: flex;
flex-direction: column;
height: 100vh;
justify-content: center;
align-items: center;
width: 100%;
background: #F5FCFE;
}
.nav {
display: inline;
padding: 60px;
}
img {
border: 1px solid rgba(0,0,0,.125);
border-radius: .25rem;
}
</style>
</head>
<body>
{% block body %}{% endblock %}
<div class="nav">
<a class="btn btn-primary" href="{{url_for('meme_rand')}}">Random</a>
<a class="btn btn-outline-primary" href="{{url_for('meme_form')}}">Creator</a>
</div>
</body>
</html>

View File

@@ -0,0 +1,5 @@
{% extends "base.html" %}
{% block title %}Meme Generator{% endblock %}
{% block body %}
<img src="{{ path }}" />
{% endblock %}

View File

@@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %}Meme Generator{% endblock %}
{% block body %}
<div class="card" style="width: 500px; max-width: 100%;">
<div class="card-body">
<form action="{{url_for('meme_post')}}" method="POST">
<div class="form-group">
<label for="image_url">Image URL</label>
<input type="url" class="form-control" id="image_url" aria-describedby="image url" placeholder="Enter a url for an image" name="image_url">
</div>
<div class="form-group">
<label for="body">Quote Body</label>
<input type="text" class="form-control" id="body" aria-describedby="Quote Body" placeholder="To be or not to be" name="body">
</div>
<div class="form-group">
<label for="author">Quote Author</label>
<input type="text" class="form-control" id="author" aria-describedby="Quote Author" placeholder="Shakespeare" name="author">
</div>
<button type="submit" class="btn btn-primary">Create Meme!</button>
</form>
</div>
</div>
{% endblock %}