Generate QR Codes from URLs in 7 File Formats
A Python script that generates a QR code from any URL and writes it out in one of seven formats: PNG, JPG, SVG, PDF, Excel, Word, or PowerPoint. Built on the `qrcode` library, it produces images that drop straight into print-ready PDFs and Office documents without any extra cropping or rescaling.
Generate a single QR code in any of seven file formats — PNG, JPG, SVG, PDF, Excel, Word, or PowerPoint — with one run. Built on the `qrcode` library, the image, print-ready PDF, and Office-document variants all come out of the same QR pattern, so name-card backs, event flyers, and team-share slides stay visually consistent.
What this script can do
- QR code generation from any URL (error-correct level M, auto version)
- Three image formats: PNG, JPG, and SVG
- Four Office formats with embedded QR: PDF, Excel, Word, PowerPoint
- Automatic PNG fallback when the SVG module is unavailable
- Timestamped filenames so repeat runs never overwrite each other
Import the .pybes file into Pybes and the script — along with its config fields — loads automatically.
Config fields
These are the config fields this script uses. Enter values through the Pybes GUI at runtime.
url Text Required URL
The URL to encode as a QR code (e.g. https://example.com)
output_format Dropdown Required Output format
Pick from three image formats (jpg / png / svg) or four Office formats (pdf / xlsx / docx / pptx)
Default: png
output_dir Folder Required Output folder
Folder where the generated QR code file will be saved
Code walkthrough
import sys
import json
import os
import io
from datetime import datetime
with open(sys.argv[1], encoding="utf-8") as f:
inputs = json.load(f)
url = inputs["url"]
output_format = inputs["output_format"]
output_dir = inputs["output_dir"]
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
print(f"Generating QR code for: {url}")
print(f"Output format: {output_format}")
try:
import qrcode
def make_qr():
qr = qrcode.QRCode(
version=None,
error_correction=qrcode.constants.ERROR_CORRECT_M,
box_size=10,
border=4,
)
qr.add_data(url)
qr.make(fit=True)
return qr
def make_png_bytes(qr):
"""Return PNG bytes of the QR code"""
img = qr.make_image(fill_color="black", back_color="white")
buf = io.BytesIO()
img.save(buf, format="PNG")
buf.seek(0)
return buf
if output_format == "jpg":
filename = f"qrcode_{timestamp}.jpg"
filepath = os.path.join(output_dir, filename)
qr = make_qr()
img = qr.make_image(fill_color="black", back_color="white")
# JPEG supports only RGB, so convert the image first
img.convert("RGB").save(filepath, "JPEG", quality=95)
elif output_format == "png":
filename = f"qrcode_{timestamp}.png"
filepath = os.path.join(output_dir, filename)
qr = make_qr()
img = qr.make_image(fill_color="black", back_color="white")
img.save(filepath, "PNG")
elif output_format == "svg":
filename = f"qrcode_{timestamp}.svg"
filepath = os.path.join(output_dir, filename)
try:
import qrcode.image.svg
qr = make_qr()
img = qr.make_image(image_factory=qrcode.image.svg.SvgPathImage)
img.save(filepath)
except ImportError:
# Fallback: generate PNG when the SVG module is unavailable
print("Warning: SVG output unavailable. Falling back to PNG.", file=sys.stderr)
filename = f"qrcode_{timestamp}.png"
filepath = os.path.join(output_dir, filename)
qr = make_qr()
img = qr.make_image(fill_color="black", back_color="white")
img.save(filepath, "PNG")
elif output_format == "pdf":
from reportlab.pdfgen import canvas
from reportlab.lib.units import mm
from reportlab.lib.utils import ImageReader
filename = f"qrcode_{timestamp}.pdf"
filepath = os.path.join(output_dir, filename)
qr = make_qr()
buf = make_png_bytes(qr)
img_reader = ImageReader(buf)
qr_size = 80 * mm
page_w = qr_size + 20 * mm
page_h = qr_size + 20 * mm
c = canvas.Canvas(filepath, pagesize=(page_w, page_h))
c.drawImage(img_reader, 10 * mm, 10 * mm, width=qr_size, height=qr_size)
c.save()
elif output_format == "xlsx":
from openpyxl import Workbook
from openpyxl.drawing.image import Image as XLImage
filename = f"qrcode_{timestamp}.xlsx"
filepath = os.path.join(output_dir, filename)
qr = make_qr()
buf = make_png_bytes(qr)
wb = Workbook()
ws = wb.active
ws.title = "QR code"
ws["A1"] = url
xl_img = XLImage(buf)
xl_img.anchor = "A2"
ws.add_image(xl_img)
wb.save(filepath)
elif output_format == "docx":
from docx import Document
from docx.shared import Mm
filename = f"qrcode_{timestamp}.docx"
filepath = os.path.join(output_dir, filename)
qr = make_qr()
buf = make_png_bytes(qr)
doc = Document()
doc.add_heading("QR code", level=1)
doc.add_paragraph(url)
doc.add_picture(buf, width=Mm(60))
doc.save(filepath)
elif output_format == "pptx":
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.text import PP_ALIGN
filename = f"qrcode_{timestamp}.pptx"
filepath = os.path.join(output_dir, filename)
qr = make_qr()
buf = make_png_bytes(qr)
prs = Presentation()
slide_layout = prs.slide_layouts[6] # Use the blank layout
slide = prs.slides.add_slide(slide_layout)
# Center the QR image on the slide
qr_size = Inches(3)
left = (prs.slide_width - qr_size) / 2
top = (prs.slide_height - qr_size) / 2
slide.shapes.add_picture(buf, left, top, width=qr_size, height=qr_size)
# Add the URL as a caption below the QR code
txBox = slide.shapes.add_textbox(left, top + qr_size, qr_size, Inches(0.4))
tf = txBox.text_frame
tf.text = url
tf.paragraphs[0].alignment = PP_ALIGN.CENTER
tf.paragraphs[0].runs[0].font.size = Pt(10)
prs.save(filepath)
else:
print(f"Error: unsupported output format: {output_format}", file=sys.stderr)
sys.exit(1)
print(f"Saved: {filepath}")
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
print("Done") The entry point reads the JSON Pybes passes via sys.argv[1] and unpacks three fields: url, output_format, and output_dir. There are no booleans or numbers to coerce — everything is either a plain string or a select value — so the assignments stay linear. A %Y%m%d_%H%M%S timestamp is built up front and reused as a filename suffix across every output branch, which keeps repeat runs from overwriting each other.
Everything that can fail lives inside a single try block so the matching except Exception at the bottom funnels every error through one code path. make_qr() owns the qrcode.QRCode configuration (error-correct level M, box size 10, quiet zone of 4 modules), and make_png_bytes() renders that QR into an in-memory PNG via io.BytesIO. The four Office branches (PDF / XLSX / DOCX / PPTX) reuse this helper instead of each re-rendering the code and writing a temp file to disk.
Image-only branches. JPG goes through img.convert("RGB") first because JPEG does not support an alpha channel and the qrcode output can be 1-bit. PNG is the simplest path — img.save(filepath, "PNG"). SVG tries qrcode.image.svg.SvgPathImage, and if the module isn't installed it silently falls back to PNG via except ImportError. Without that fallback, a missing optional module could yield a run that exits "successfully" with zero output files.
Office branches and the shared epilogue. Each one pulls a PNG buffer out of make_png_bytes() and feeds it to the respective library: reportlab draws the QR on a custom-sized PDF page, openpyxl anchors the image to cell A2 with the URL in A1, python-docx adds an H1 heading + paragraph + picture, and python-pptx centers the QR on a blank slide (slide_layouts[6]) with the URL as a captioned textbox. The final print("Done") only fires when no branch called sys.exit(1), so a Done line in the log means the file is on disk.
How it works
Error-correct level M is the sweet spot
qrcode.QRCode is initialized with error_correction=qrcode.constants.ERROR_CORRECT_M, which can recover about 15% of a damaged code. The four levels are L (7%) / M (15%) / Q (25%) / H (30%). M is the QR spec's default and handles typical print smudges and creasing. Bump it to H when you plan to overlay a logo, since you're effectively removing a chunk of the pattern on purpose.
make_qr and make_png_bytes deduplicate the image work
make_qr() owns the QRCode(...) configuration, and make_png_bytes() renders that pattern to an in-memory PNG via io.BytesIO. The PDF, Excel, Word, and PowerPoint branches all embed a PNG, so they pull from the same buffer instead of each re-rendering the QR. Because the buffer lives in memory, the Office libraries read it directly with no temp files to clean up on disk.
One big if / elif dispatches by format
After the helpers, a long if output_format == ... chain handles each format. Image formats save directly; Office formats instantiate a document and call add_image / drawImage / add_picture. Per-format quirks like img.convert("RGB") for JPEG or prs.slide_layouts[6] for PowerPoint's blank layout live inside each branch, so the outer structure stays uniform and easy to extend.
Customization
Change the QR size
Adjust box_size=10 inside make_qr(). That value is the pixel size of one QR module. 5 gives a compact QR, 15 gives a large one. border=4 is the quiet-zone width in modules — the QR spec recommends 4, and shrinking it will hurt scan reliability in poor lighting.
Recolor the QR code
Replace fill_color="black" and back_color="white" with any Pillow-compatible color string ("#1E40AF", "navy", etc.) to hit brand colors. Keep contrast strong — phone cameras struggle once the foreground/background pair gets visually close. Light background plus dark foreground is the safest combination.
Overlay a logo with higher error correction
Switch ERROR_CORRECT_M to ERROR_CORRECT_H so the QR tolerates up to about 30% loss, then paste your logo over the center of img in make_png_bytes using Pillow's Image.paste(). With H-level correction the QR still scans even with a logo covering part of the pattern.
Troubleshooting
qrcode.exceptions.DataOverflowError crashes the run
The URL exceeds the QR spec's maximum capacity (version 40, about 4,000 bytes). At error-correct level M the alphanumeric ceiling is around 2,300 characters, and going past it raises DataOverflowError: Code length overflow. Shorten the URL through a service like bit.ly or TinyURL and rerun, or drop ERROR_CORRECT_M to ERROR_CORRECT_L if you need to keep the long URL as-is (L gives more data capacity).
Japanese or non-ASCII URLs come out garbled
qrcode stores UTF-8 bytes, so multibyte characters encode fine. The problem usually sits on the reader side — older QR scanners assume Shift_JIS and decode incorrectly. Safer option: run the URL through urllib.parse.quote(url, safe=":/?#&=") before add_data so the QR carries percent-encoded ASCII, which every reader handles.
PermissionError while saving the output file
A same-named file is open in Excel / Word / PowerPoint and Windows holds a file lock, so wb.save / doc.save / prs.save fails. Close the open app and rerun — because filenames carry a %Y%m%d_%H%M%S timestamp, you won't collide unless you rerun inside the same second. If the output folder itself has no write access, pick a writable folder like C:\Users\<you>\Documents instead.
FAQ
Why is SVG the only format with an automatic fallback?
qrcode.image.svg depends on an extra submodule layer that can be silently absent at runtime. PNG and JPG ride on Pillow, which is a core dependency — if it's missing you know immediately. SVG can install cleanly and then fail only when the script runs, so catching ImportError avoids an "it ran but produced nothing" scenario.
Why is error-correct level M the default?
It's the QR spec's default and balances damage tolerance against pattern size. L (7%) is risky on printed materials, H (30%) inflates the code and makes it denser. M survives typical print degradation while keeping the QR visually compact and quick to generate.
Can I batch multiple URLs in a single run?
Not without modification — this script is intentionally one-URL-per-run. To process a list, wrap the body in a for url in urls: loop (or read URLs from a CSV) and add a per-URL counter into the filename. A dedicated batch-QR script is cleaner than shoehorning that loop in here.
Can I reposition the embedded QR inside the Office file?
Yes. The output is a plain PPTX / DOCX / XLSX, so open it in PowerPoint / Word / Excel and drag the image anywhere. For programmatic placement, tweak the add_picture / drawImage / xl_img.anchor parameters at the end of each Office branch — size and position are inline in the script.
Import the .pybes file into Pybes and the script — along with its config fields — loads automatically.