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

3
.gitignore vendored
View File

@@ -174,3 +174,6 @@ cython_debug/
# PyPI configuration file
.pypirc
# VS Code
.vscode/

View File

@@ -21,6 +21,7 @@ class NEODatabase:
help fetch NEOs by primary designation or by name and to help speed up
querying for close approaches that match criteria.
"""
def __init__(self, neos, approaches):
"""Create a new `NEODatabase`.
@@ -42,9 +43,16 @@ class NEODatabase:
self._neos = neos
self._approaches = approaches
# TODO: What additional auxiliary data structures will be useful?
self._neo_by_designation = {neo.designation: neo for neo in self._neos}
self._neo_by_name = {neo.name: neo for neo in self._neos}
# TODO: Link together the NEOs and their close approaches.
for approach in self._approaches:
# Link approach.neo to the corresponding NearEarthObject, namely
# the one whose designation matches approach._designation.
approach.neo = self._neo_by_designation[approach._designation]
# Add this approach to the corresponding NearEarthObject's
# .approaches collection.
self._neo_by_designation[approach._designation].approaches.append(approach)
def get_neo_by_designation(self, designation):
"""Find and return an NEO by its primary designation.
@@ -59,8 +67,7 @@ class NEODatabase:
:param designation: The primary designation of the NEO to search for.
:return: The `NearEarthObject` with the desired primary designation, or `None`.
"""
# TODO: Fetch an NEO by its primary designation.
return None
return self._neo_by_designation.get(designation, None)
def get_neo_by_name(self, name):
"""Find and return an NEO by its name.
@@ -76,8 +83,7 @@ class NEODatabase:
:param name: The name, as a string, of the NEO to search for.
:return: The `NearEarthObject` with the desired name, or `None`.
"""
# TODO: Fetch an NEO by its name.
return None
return self._neo_by_name.get(name, None)
def query(self, filters=()):
"""Query close approaches to generate those that match a collection of filters.
@@ -93,6 +99,7 @@ class NEODatabase:
:param filters: A collection of filters capturing user-specified criteria.
:return: A stream of matching `CloseApproach` objects.
"""
# TODO: Generate `CloseApproach` objects that match all of the filters.
for approach in self._approaches:
yield approach
cases = [filter_i(approach) for filter_i in filters]
if all(cases):
yield approach

View File

@@ -12,9 +12,9 @@ line, and uses the resulting collections to build an `NEODatabase`.
You'll edit this file in Task 2.
"""
import csv
import json
from models import NearEarthObject, CloseApproach
@@ -24,8 +24,16 @@ def load_neos(neo_csv_path):
:param neo_csv_path: A path to a CSV file containing data about near-Earth objects.
:return: A collection of `NearEarthObject`s.
"""
# TODO: Load NEO data from the given CSV file.
return ()
neos = []
with open(neo_csv_path, "r") as csv_file:
reader = csv.reader(csv_file)
next(reader)
for row in reader:
neo = NearEarthObject(
designation=row[3], name=row[4], diameter=row[15], hazardous=row[7]
)
neos.append(neo)
return neos
def load_approaches(cad_json_path):
@@ -34,5 +42,17 @@ def load_approaches(cad_json_path):
:param cad_json_path: A path to a JSON file containing data about close approaches.
:return: A collection of `CloseApproach`es.
"""
# TODO: Load close approach data from the given JSON file.
return ()
approaches = []
with open(cad_json_path, "r") as json_file:
cad = json.load(json_file)
for item in cad["data"]:
# 0: pdes, 3: cd, 4: dist, 7: v_rel
approach = CloseApproach(
designation=item[0],
time=item[3],
distance=float(item[4]),
velocity=float(item[7]),
)
approaches.append(approach)
return approaches

View File

@@ -16,7 +16,9 @@ iterator.
You'll edit this file in Tasks 3a and 3c.
"""
import operator
import itertools
class UnsupportedCriterionError(NotImplementedError):
@@ -38,6 +40,7 @@ class AttributeFilter:
Concrete subclasses can override the `get` classmethod to provide custom
behavior to fetch a desired attribute from the given `CloseApproach`.
"""
def __init__(self, op, value):
"""Construct a new `AttributeFilter` from an binary predicate and a reference value.
@@ -72,12 +75,57 @@ class AttributeFilter:
return f"{self.__class__.__name__}(op=operator.{self.op.__name__}, value={self.value})"
class DistanceFilter(AttributeFilter):
"""Subclass of AttributeFilter to filter by distance attribute."""
@classmethod
def get(cls, approach):
return approach.distance
class VelocityFilter(AttributeFilter):
"""Subclass of AttributeFilter to filter by velocity attribute."""
@classmethod
def get(cls, approach):
return approach.velocity
class DateFilter(AttributeFilter):
"""Subclass of AttributeFilter to filter by date attribute."""
@classmethod
def get(cls, approach):
return approach.time.date()
class DiameterFilter(AttributeFilter):
"""Subclass of AttributeFilter to filter by diameter attribute."""
@classmethod
def get(cls, approach):
return approach.neo.diameter
class HazardousFilter(AttributeFilter):
"""Subclass of AttributeFilter to filter by hazardous attribute."""
@classmethod
def get(cls, approach):
return approach.neo.hazardous
def create_filters(
date=None, start_date=None, end_date=None,
distance_min=None, distance_max=None,
velocity_min=None, velocity_max=None,
diameter_min=None, diameter_max=None,
hazardous=None
date=None,
start_date=None,
end_date=None,
distance_min=None,
distance_max=None,
velocity_min=None,
velocity_max=None,
diameter_min=None,
diameter_max=None,
hazardous=None,
):
"""Create a collection of filters from user-specified criteria.
@@ -108,8 +156,30 @@ def create_filters(
:param hazardous: Whether the NEO of a matching `CloseApproach` is potentially hazardous.
:return: A collection of filters for use with `query`.
"""
# TODO: Decide how you will represent your filters.
return ()
filters = []
if date:
filters.append(DateFilter(operator.eq, date))
if start_date:
filters.append(DateFilter(operator.ge, start_date))
if end_date:
filters.append(DateFilter(operator.le, end_date))
if distance_min:
filters.append(DistanceFilter(operator.ge, distance_min))
if distance_max:
filters.append(DistanceFilter(operator.le, distance_max))
if velocity_min:
filters.append(VelocityFilter(operator.ge, velocity_min))
if velocity_max:
filters.append(VelocityFilter(operator.le, velocity_max))
if diameter_min:
filters.append(DiameterFilter(operator.ge, diameter_min))
if diameter_max:
filters.append(DiameterFilter(operator.le, diameter_max))
if hazardous is not None:
filters.append(HazardousFilter(operator.eq, hazardous))
return filters
def limit(iterator, n=None):
@@ -121,5 +191,4 @@ def limit(iterator, n=None):
:param n: The maximum number of values to produce.
:yield: The first (at most) `n` values from the iterator.
"""
# TODO: Produce at most `n` values from the given iterator.
return iterator
return itertools.islice(iterator, n) if n else iterator

View File

@@ -17,7 +17,10 @@ quirks of the data set, such as missing names and unknown diameters.
You'll edit this file in Task 1.
"""
from helpers import cd_to_datetime, datetime_to_str
import math
from datetime import datetime
class NearEarthObject:
@@ -32,22 +35,20 @@ class NearEarthObject:
initialized to an empty collection, but eventually populated in the
`NEODatabase` constructor.
"""
# TODO: How can you, and should you, change the arguments to this constructor?
# If you make changes, be sure to update the comments in this file.
def __init__(self, **info):
"""Create a new `NearEarthObject`.
:param info: A dictionary of excess keyword arguments supplied to the constructor.
"""
# TODO: Assign information from the arguments passed to the constructor
# onto attributes named `designation`, `name`, `diameter`, and `hazardous`.
# You should coerce these values to their appropriate data type and
# handle any edge cases, such as a empty name being represented by `None`
# and a missing diameter being represented by `float('nan')`.
self.designation = ''
self.name = None
self.diameter = float('nan')
self.hazardous = False
self.designation = info.get("designation")
self.name = None if info.get("name") == "" else info.get("name")
self.diameter = (
float("nan")
if info.get("diameter") in (None, "")
else float(info.get("diameter"))
)
self.hazardous = info.get("hazardous", "N") == "Y"
# Create an empty initial collection of linked approaches.
self.approaches = []
@@ -55,20 +56,36 @@ class NearEarthObject:
@property
def fullname(self):
"""Return a representation of the full name of this NEO."""
# TODO: Use self.designation and self.name to build a fullname for this object.
return ''
if self.name:
return f"{self.designation} {self.name}"
return f"{self.designation}"
def __str__(self):
"""Return `str(self)`."""
# TODO: Use this object's attributes to return a human-readable string representation.
# The project instructions include one possibility. Peek at the __repr__
# method for examples of advanced string formatting.
return f"A NearEarthObject ..."
hazardous_str = (
"is potentially hazardous"
if self.hazardous
else "is not potentially hazardous"
)
if not math.isnan(self.diameter):
return f"NEO {self.fullname} has a diameter of {self.diameter:.3f} and {hazardous_str}."
return f"NEO {self.fullname} has an unknown diameter and {hazardous_str}."
def __repr__(self):
"""Return `repr(self)`, a computer-readable string representation of this object."""
return f"NearEarthObject(designation={self.designation!r}, name={self.name!r}, " \
f"diameter={self.diameter:.3f}, hazardous={self.hazardous!r})"
return (
f"NearEarthObject(designation={self.designation!r}, name={self.name!r}, "
f"diameter={self.diameter:.3f}, hazardous={self.hazardous!r})"
)
def serialize(self):
"""Serialize this `NearEarthObject` into a dictionary."""
return {
"designation": self.designation,
"name": self.name,
"diameter_km": self.diameter,
"potentially_hazardous": self.hazardous,
}
class CloseApproach:
@@ -84,21 +101,20 @@ class CloseApproach:
private attribute, but the referenced NEO is eventually replaced in the
`NEODatabase` constructor.
"""
# TODO: How can you, and should you, change the arguments to this constructor?
# If you make changes, be sure to update the comments in this file.
def __init__(self, **info):
"""Create a new `CloseApproach`.
:param info: A dictionary of excess keyword arguments supplied to the constructor.
"""
# TODO: Assign information from the arguments passed to the constructor
# onto attributes named `_designation`, `time`, `distance`, and `velocity`.
# You should coerce these values to their appropriate data type and handle any edge cases.
# The `cd_to_datetime` function will be useful.
self._designation = ''
self.time = None # TODO: Use the cd_to_datetime function for this attribute.
self.distance = 0.0
self.velocity = 0.0
self._designation = info.get("designation")
self.time = info.get("time")
if self.time:
self.time = cd_to_datetime(info.get("time"))
self.distance = info.get("distance", float("nan"))
self.velocity = info.get("velocity", float("nan"))
# Create an attribute for the referenced NEO, originally None.
self.neo = None
@@ -116,19 +132,23 @@ class CloseApproach:
formatted string that can be used in human-readable representations and
in serialization to CSV and JSON files.
"""
# TODO: Use this object's `.time` attribute and the `datetime_to_str` function to
# build a formatted representation of the approach time.
# TODO: Use self.designation and self.name to build a fullname for this object.
return ''
return f"{datetime_to_str(self.time)}"
def __str__(self):
"""Return `str(self)`."""
# TODO: Use this object's attributes to return a human-readable string representation.
# The project instructions include one possibility. Peek at the __repr__
# method for examples of advanced string formatting.
return f"A CloseApproach ..."
return f"On {self.time_str}, {self.neo.fullname} approaches Earth at a distance of {self.distance:.2f} au and a velocity of {self.velocity:.2f} km/s."
def __repr__(self):
"""Return `repr(self)`, a computer-readable string representation of this object."""
return f"CloseApproach(time={self.time_str!r}, distance={self.distance:.2f}, " \
f"velocity={self.velocity:.2f}, neo={self.neo!r})"
return (
f"CloseApproach(time={self.time_str!r}, distance={self.distance:.2f}, "
f"velocity={self.velocity:.2f}, neo={self.neo!r})"
)
def serialize(self):
"""Serialize this `CloseApproach` into a dictionary."""
return {
"datetime_utc": self.time_str,
"distance_au": self.distance,
"velocity_km_s": self.velocity,
}

View File

@@ -10,6 +10,7 @@ extension determines which of these functions is used.
You'll edit this file in Part 4.
"""
import csv
import json
@@ -25,10 +26,24 @@ def write_to_csv(results, filename):
:param filename: A Path-like object pointing to where the data should be saved.
"""
fieldnames = (
'datetime_utc', 'distance_au', 'velocity_km_s',
'designation', 'name', 'diameter_km', 'potentially_hazardous'
"datetime_utc",
"distance_au",
"velocity_km_s",
"designation",
"name",
"diameter_km",
"potentially_hazardous",
)
# TODO: Write the results to a CSV file, following the specification in the instructions.
with open(filename, "w") as csv_file:
writer = csv.DictWriter(csv_file, fieldnames)
writer.writeheader()
for result in results:
row = {**result.serialize(), **result.neo.serialize()}
row["name"] = "" if row["name"] is None else row["name"]
row["diameter_km"] = (
"" if row["diameter_km"] is float("nan") else row["diameter_km"]
)
writer.writerow(row)
def write_to_json(results, filename):
@@ -42,4 +57,25 @@ def write_to_json(results, filename):
:param results: An iterable of `CloseApproach` objects.
:param filename: A Path-like object pointing to where the data should be saved.
"""
# TODO: Write the results to a JSON file, following the specification in the instructions.
data = []
for result in results:
item = {**result.serialize(), **result.neo.serialize()}
item["name"] = "" if item["name"] is None else item["name"]
item["diameter_km"] = (
"" if item["diameter_km"] is float("nan") else item["diameter_km"]
)
data.append(
{
"datetime_utc": item["datetime_utc"],
"distance_au": item["distance_au"],
"velocity_km_s": item["velocity_km_s"],
"neo": {
"designation": item["designation"],
"name": item["name"],
"diameter_km": item["diameter_km"],
"potentially_hazardous": item["potentially_hazardous"],
},
}
)
with open(filename, "w") as json_file:
json.dump(data, json_file, indent=2)

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 %}