The text pages go through Platypus: content flows into a frame, and when the frame is full a new page opens with the same header and footer. Floor plans and galleries do not flow; they get a separate layout pass with fixed positions. Both paths end on the same canvas, so the footers still count correctly.
Services
Backend Engineering, Document Automation
Industry
Real Estate
Year
2022-2024
The 45-minute exposé
One property exposé took the marketing team 45 minutes in InDesign. The platform had more than 200 active listings, and every change to a listing meant building its document again by hand. Within weeks the team was behind, and the backlog only grew. The listings were live. The documents that belonged to them were not.
THE CHALLENGE
Manual layout, 200 listings, no template
Every exposé had the same parts: a cover with a hero image and a title block that had to survive titles from 10 to 90 characters, a factsheet with configurable fields, and up to eight further pages of descriptions, floor plans and galleries. The marketing team rebuilt all of it in InDesign, one listing at a time, 45 minutes each. An updated price or a new photo meant the whole document again. Nothing was templated and nothing was versioned, so no two people produced quite the same layout, and every new hire drifted a little further from the brand.
THE SOLUTION
A generator instead of a converter
The quick route would have been HTML to PDF with wkhtmltopdf or WeasyPrint: write the exposé as a web page and let a browser engine lay it out. It works for simple documents and fails at exactly this one: sub-millimetre placement of images against the A4 spec, SVG logos at fixed coordinates, and footers that need a page total the engine does not have yet. I chose ReportLab instead and wrote a CompanyPDF class, about 1,700 lines of Python, that draws the document procedurally. Platypus flows the text and paginates, the cover measures its title and shrinks the font until it fits its box, and a small canvas subclass holds every page back until the last one exists. The price is more code and no CSS. The return is output that is identical on every server, with no browser engine to install, patch or keep in sync.
The cover's title loop: measure, shrink by half a point, measure again, until the string fits the box or reaches 16 pt:
Python
def _draw_title(self, canvas, title, box_width, box_y):
font_size = 42 # Start large
min_size = 16
while font_size > min_size:
w = canvas.stringWidth(title, self.font_bold, font_size)
if w <= box_width:
break
font_size -= 0.5
canvas.setFont(self.font_bold, font_size)
canvas.drawString(self.margin_left, box_y, title)Fit a title, then number the pages
Type a property title or pick a sample. The sheet is a scaled A4 page and runs the same loop as the generator: 42 pt down in 0.5 pt steps until the text fits the 515 pt box, never below 16 pt. Below it, watch the footers: every page says 'of ?' until save() runs.
Penthouse Suite with Panoramic Views
Berlin · 4 rooms · 142 m²- Font size
- 25.5 pt
- Shrink steps
- 33
- Text width
- 514 / 515 pt
- Result
- Fits on one line
Deferred page numbering7 pages numbered at save()
- Page 1 of 7
- Page 2 of 7
- Page 3 of 7
- Page 4 of 7
- Page 5 of 7
- Page 6 of 7
- Page 7 of 7
The canvas subclass behind 'Page X of Y'. showPage() buffers, save() numbers and writes:
Python
class NumberedCanvas(canvas.Canvas):
"""Defers page numbering until save()."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._saved_pages = []
def showPage(self):
# Buffer the page instead of emitting it
self._saved_pages.append(dict(self.__dict__))
self._startPage()
def save(self):
total = len(self._saved_pages)
for i, state in enumerate(self._saved_pages, 1):
self.__dict__.update(state)
self.setFont("Helvetica", 9)
self.drawRightString(
self._pagesize[0] - 30,
20,
f"Page {i} of {total}"
)
super().showPage()
super().save()THE RESULT
The marketing team stopped making PDFs
The generator took over every exposé. Change a listing and the next request returns a new document; nobody opens InDesign, nobody checks whether the footer still says the right page count. The person who used to spend full days on property PDFs was moved to other work, and the brand looks the same on every document because exactly one place defines how it looks. Turnaround went from most of an hour to a few seconds, and the queue that had grown for weeks was simply gone.
KEY METRICS
<3sPer exposé, generated
6,000+Documents in 18 months
1,700Lines of Python
CLIENT FEEDBACK
We had one person whose week was property PDFs: new listings, price changes, new photos. That work is gone. The documents come out of the platform, they all look the same, and that role now does something else.
Managing Director
Real estate platform, operations
FOR YOUR PROJECT
- When it applies
You produce the same branded document again and again from structured data, and a person rebuilds it by hand after every change. Property exposés here; offers, reports and certificates have the same shape.
- What to check
Whether everything the document needs already lives in one record: title, price, fields, images, the link for the QR code. If parts of it exist only in a designer's file, the generator has nothing to read. Then count documents per month and minutes per document.
- What it needs
A Python backend developer, ReportLab, brand assets as SVG, and a test set of real titles from 10 to 90 characters. No browser engine on the server. This one is about 1,700 lines for a cover, a factsheet and up to eight content pages.
FAQ
TECHNOLOGY STACK
Django
Python
Manuel Kasbarian - CEO, SophistiXWe have enjoyed working with Daniel for 10 years now. We highly appreciate his fast response times around the clock and his all-round knowledge. Whether server configurations or programming, he always has the right solution.
Follow in the footsteps of Manuel and bring your vision to life.
Open to new ProjectsGet In Touch