DIY Funny Giftbooks: A Creative and Personal Birthday Gift
Looking for a unique and funny birthday gift for a friend? Instead of buying a ready-made giftbook online, why not create your own? With a little help from Python and some humorous content from Mastodon, you can craft a personalized giftbook that’s sure to bring smiles.
Step 1: Gather Your Content
One great source of funny content is the Mastodon channel @punypunypuns
, which shares a collection of puns illustrated with images or simply as text in German. I’ve created a Python script that downloads all the posts from this channel and compiles them into a PDF, perfect for turning into a giftbook. The channel is mine, so you won’t run into any copyright issues as long as you don’t sell the booklets.
Step 2: Use the Script to Create Your PDF
The script is tailored for the @punypunypuns
channel but can be easily adapted for other Mastodon channels. If you want to use it for another channel, just make sure you have permission from the content owner. The script extracts the posts, cleans up the text, and compiles everything into a PDF file. You can find the script here on GitLab or below.
Step 3: Turn the PDF into a Booklet
Once you have your PDF, you can use a tool like online2pdf.com to transform it into an A5 booklet. This tool rearranges the pages so that when printed on A4 paper, folded in half, and bound, the pages are in the correct order.
Step 4: Binding Your Booklet with a Tacker
Binding your booklet is easy using a tacker (stapler). Here’s how:
-
Print the Booklet: After creating the A5 booklet, print it on A4 paper, making sure to print double-sided.
-
Fold the Pages: Fold the stack of printed pages in half, aligning the edges carefully.
-
Staple the Spine: Use a tacker to staple the folded edge (spine) of the booklet. Place two or three staples evenly along the spine to ensure the pages are securely held together.
And there you have it—a handmade, funny giftbook that’s personal, creative, and guaranteed to make your friend’s day!
import os
import json
from datetime import datetime
from mastodon import Mastodon
from bs4 import BeautifulSoup
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import inch
from reportlab.lib.utils import ImageReader
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import (
SimpleDocTemplate,
Paragraph,
Spacer,
Image,
PageBreak,
Frame,
PageTemplate,
)
# Load configuration from config.json
# {
# "client_id": "your_client_id",
# "client_secret": "your_client_secret",
# "access_token": "your_access_token",
# "api_base_url": "https://your.instance.url"
# }
with open("config.json", "r") as config_file:
config = json.load(config_file)
# Set up Mastodon API instance
mastodon = Mastodon(
client_id=config["client_id"],
client_secret=config["client_secret"],
access_token=config["access_token"],
api_base_url=config["api_base_url"],
)
# Function to fetch all toots from a user
def fetch_all_toots(username):
user = mastodon.account_search(username)[0]
user_id = user["id"]
toots = []
max_id = None
while True:
if max_id:
new_toots = mastodon.account_statuses(user_id, max_id=max_id)
else:
new_toots = mastodon.account_statuses(user_id)
if not new_toots:
break
toots.extend(new_toots)
max_id = new_toots[-1]["id"] - 1
return toots
# Custom JSON encoder to handle datetime objects
class DateTimeEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
return super(DateTimeEncoder, self).default(obj)
# Fetch all toots from the user @punypunypuns
username = "punypunypuns"
toots = fetch_all_toots(username)
# Create output directory if it does not exist
output_dir = "punypunypuns"
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# Save toots to a file
output_file = os.path.join(output_dir, f"{username}_toots.json")
with open(output_file, "w", encoding="utf-8") as f:
json.dump(toots, f, ensure_ascii=False, indent=4, cls=DateTimeEncoder)
print(f"All toots from @{username} have been downloaded and saved to {output_file}")
# Extract the required properties, remove HTML tags from content, and skip toots containing "yanzi"
extracted_data = []
for toot in toots:
content = BeautifulSoup(toot["content"], "html.parser").get_text()
if "yanzi" in content.lower():
continue
toot_data = {
"content": content,
"media_attachments": [media["url"] for media in toot["media_attachments"]],
}
extracted_data.append(toot_data)
# Save the extracted data to another JSON file
extracted_output_file = os.path.join(output_dir, f"{username}_extracted_toots.json")
with open(extracted_output_file, "w", encoding="utf-8") as f:
json.dump(extracted_data, f, ensure_ascii=False, indent=4)
print(f"Extracted data from toots have been saved to {extracted_output_file}")
# Generate a PDF file with the extracted toots
pdf_file = os.path.join(output_dir, f"{username}_toots.pdf")
doc = SimpleDocTemplate(pdf_file, pagesize=A4)
width, height = A4
styles = getSampleStyleSheet()
font_size = 16
# Define custom styles for the title, subtitle, and content
title_style = ParagraphStyle(
"Title",
parent=styles["Normal"],
fontSize=36,
leading=42,
alignment=1, # Center alignment
spaceAfter=20,
)
subtitle_style = ParagraphStyle(
"Subtitle",
parent=styles["Normal"],
fontSize=24,
leading=28,
alignment=1, # Center alignment
spaceAfter=40,
)
content_style = ParagraphStyle(
"Content",
parent=styles["Normal"],
fontSize=font_size,
leading=font_size * 1.2,
alignment=1, # Center alignment
spaceAfter=12,
)
story = []
# Add the title page
title = Paragraph("puny puny puns", title_style)
subtitle = Paragraph("von Sonja und Lorenz", subtitle_style)
# Center the title and subtitle vertically and horizontally
title_page_frame = Frame(
doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id="title_page_frame"
)
def title_page(canvas, doc):
canvas.saveState()
title_page_frame.addFromList([Spacer(1, height / 2.5), title, subtitle], canvas)
canvas.restoreState()
title_page_template = PageTemplate(
id="TitlePage", frames=title_page_frame, onPage=title_page
)
content_page_template = PageTemplate(
id="ContentPage",
frames=[
Frame(
doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id="content_frame"
)
],
)
doc.addPageTemplates([title_page_template, content_page_template])
# Add the title page and then a page break
story.append(PageBreak())
# Add the extracted toots
for toot in extracted_data:
if toot["media_attachments"]:
# Load the image
image_url = toot["media_attachments"][0]
try:
image = ImageReader(image_url)
image_width, image_height = image.getSize()
aspect_ratio = image_height / float(image_width)
# Determine the display size while maintaining the aspect ratio
max_display_width = width * 0.75
max_display_height = height * 0.75
if image_width > max_display_width or image_height > max_display_height:
if (max_display_width / image_width) < (
max_display_height / image_height
):
display_width = max_display_width
display_height = display_width * aspect_ratio
else:
display_height = max_display_height
display_width = display_height / aspect_ratio
else:
display_width = image_width
display_height = image_height
image_element = Image(image_url, width=display_width, height=display_height)
story.append(image_element)
except Exception as e:
print(f"Error loading image {image_url}: {e}")
pass
# Add a spacer to vertically center the text if no images are present
if not toot["media_attachments"]:
story.append(Spacer(1, height / 2.5))
# Add the text
content = Paragraph(toot["content"], content_style)
story.append(content)
story.append(PageBreak()) # Add a page break after each toot
doc.build(story)
print(f"PDF file with toots has been generated: {pdf_file}")
# Delete the JSON files if the PDF was successfully created
if os.path.exists(pdf_file):
os.remove(output_file)
os.remove(extracted_output_file)
print("JSON files have been deleted.")
else:
print("PDF file was not created successfully. JSON files have not been deleted.")