Initial commit: Spork card PDF compositor
Python library for building print-ready PDFs by layering transparent PNGs and JPGs. Supports image cropping, page fills, lines for crop marks, and region copying for tiling cards across a sheet. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
175
spork.py
Normal file
175
spork.py
Normal file
@@ -0,0 +1,175 @@
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.pdfgen import canvas
|
||||
from PIL import Image, ImageDraw
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
# Colors (R, G, B)
|
||||
BLACK = (0, 0, 0)
|
||||
WHITE = (255, 255, 255)
|
||||
RED = (255, 0, 0)
|
||||
GREEN = (0, 128, 0)
|
||||
BLUE = (0, 0, 255)
|
||||
YELLOW = (255, 255, 0)
|
||||
CYAN = (0, 255, 255)
|
||||
MAGENTA = (255, 0, 255)
|
||||
GRAY = (128, 128, 128)
|
||||
LIGHT_GRAY = (192, 192, 192)
|
||||
DARK_GRAY = (64, 64, 64)
|
||||
|
||||
|
||||
class LoadedImage:
|
||||
def __init__(self, filepath):
|
||||
if not os.path.exists(filepath):
|
||||
raise FileNotFoundError(f"Image not found: {filepath}")
|
||||
self.filepath = filepath
|
||||
self.image = Image.open(filepath)
|
||||
|
||||
def CropPixelsL(self, pixels):
|
||||
w, h = self.image.size
|
||||
self.image = self.image.crop((pixels, 0, w, h))
|
||||
return self
|
||||
|
||||
def CropPixelsR(self, pixels):
|
||||
w, h = self.image.size
|
||||
self.image = self.image.crop((0, 0, w - pixels, h))
|
||||
return self
|
||||
|
||||
def CropPixelsT(self, pixels):
|
||||
w, h = self.image.size
|
||||
self.image = self.image.crop((0, pixels, w, h))
|
||||
return self
|
||||
|
||||
def CropPixelsB(self, pixels):
|
||||
w, h = self.image.size
|
||||
self.image = self.image.crop((0, 0, w, h - pixels))
|
||||
return self
|
||||
|
||||
def CropSquare(self):
|
||||
w, h = self.image.size
|
||||
if w > h:
|
||||
excess = w - h
|
||||
left = excess // 2
|
||||
self.image = self.image.crop((left, 0, left + h, h))
|
||||
elif h > w:
|
||||
excess = h - w
|
||||
top = excess // 2
|
||||
self.image = self.image.crop((0, top, w, top + w))
|
||||
return self
|
||||
|
||||
def KeepPixelsT(self, pixels):
|
||||
w, h = self.image.size
|
||||
self.image = self.image.crop((0, 0, w, pixels))
|
||||
return self
|
||||
|
||||
def KeepPixelsB(self, pixels):
|
||||
w, h = self.image.size
|
||||
self.image = self.image.crop((0, h - pixels, w, h))
|
||||
return self
|
||||
|
||||
def KeepPixelsL(self, pixels):
|
||||
w, h = self.image.size
|
||||
self.image = self.image.crop((0, 0, pixels, h))
|
||||
return self
|
||||
|
||||
def KeepPixelsR(self, pixels):
|
||||
w, h = self.image.size
|
||||
self.image = self.image.crop((w - pixels, 0, w, h))
|
||||
return self
|
||||
|
||||
def KeepSquare(self, pixels):
|
||||
w, h = self.image.size
|
||||
cx, cy = w // 2, h // 2
|
||||
half = pixels // 2
|
||||
self.image = self.image.crop((cx - half, cy - half, cx - half + pixels, cy - half + pixels))
|
||||
return self
|
||||
|
||||
|
||||
def ReadPNG(filepath):
|
||||
return LoadedImage(filepath)
|
||||
|
||||
|
||||
def ReadJPG(filepath):
|
||||
return LoadedImage(filepath)
|
||||
|
||||
|
||||
DPI = 300
|
||||
|
||||
|
||||
class NewPDF:
|
||||
def __init__(self, width_inches, height_inches):
|
||||
self.width_inches = width_inches
|
||||
self.height_inches = height_inches
|
||||
self.width = width_inches * inch
|
||||
self.height = height_inches * inch
|
||||
self._ops = []
|
||||
|
||||
def layer(self, img, x_inches, y_inches, w_inches, h_inches):
|
||||
self._ops.append(("layer", img, x_inches, y_inches, w_inches, h_inches))
|
||||
|
||||
def copy(self, x0, y0, x1, y1, x2, y2):
|
||||
self._ops.append(("copy", x0, y0, x1, y1, x2, y2))
|
||||
|
||||
def fill(self, color):
|
||||
self._ops.append(("fill", color))
|
||||
|
||||
def line(self, color, x0, y0, x1, y1, width=1):
|
||||
self._ops.append(("line", color, x0, y0, x1, y1, width))
|
||||
|
||||
def save(self, filename):
|
||||
pw = int(self.width_inches * DPI)
|
||||
ph = int(self.height_inches * DPI)
|
||||
page = Image.new("RGBA", (pw, ph), (255, 255, 255, 255))
|
||||
|
||||
for op in self._ops:
|
||||
if op[0] == "layer":
|
||||
_, img, x, y, w, h = op
|
||||
xp = int(x * DPI)
|
||||
yp = int(y * DPI)
|
||||
wp = int(w * DPI)
|
||||
hp = int(h * DPI)
|
||||
resized = img.image.resize((wp, hp), Image.LANCZOS)
|
||||
if resized.mode == "RGBA":
|
||||
page.paste(resized, (xp, yp), resized)
|
||||
else:
|
||||
page.paste(resized, (xp, yp))
|
||||
elif op[0] == "copy":
|
||||
_, x0, y0, x1, y1, x2, y2 = op
|
||||
region = page.crop((
|
||||
int(x0 * DPI), int(y0 * DPI),
|
||||
int(x1 * DPI), int(y1 * DPI),
|
||||
))
|
||||
page.paste(region, (int(x2 * DPI), int(y2 * DPI)))
|
||||
elif op[0] == "fill":
|
||||
_, color = op
|
||||
page.paste(color + (255,), (0, 0, pw, ph))
|
||||
elif op[0] == "line":
|
||||
_, color, x0, y0, x1, y1, width = op
|
||||
draw = ImageDraw.Draw(page)
|
||||
draw.line(
|
||||
[(int(x0 * DPI), int(y0 * DPI)),
|
||||
(int(x1 * DPI), int(y1 * DPI))],
|
||||
fill=color + (255,), width=width,
|
||||
)
|
||||
|
||||
# Write composited image to PDF
|
||||
rgb_page = page.convert("RGB")
|
||||
c = canvas.Canvas(filename, pagesize=(self.width, self.height))
|
||||
tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
|
||||
try:
|
||||
rgb_page.save(tmp.name, "PNG")
|
||||
c.drawImage(tmp.name, 0, 0, self.width, self.height)
|
||||
finally:
|
||||
os.unlink(tmp.name)
|
||||
c.save()
|
||||
print(f"Saved: {filename}")
|
||||
|
||||
|
||||
# ── Example usage ──
|
||||
if __name__ == "__main__":
|
||||
outfile = NewPDF(8.5, 11)
|
||||
a = ReadPNG("mushroom.png")
|
||||
b = ReadPNG("PictureFrame.png")
|
||||
outfile.layer(a, 0.5, 0.5, 3.5, 3.5)
|
||||
outfile.layer(b, 0.5, 0.5, 3.5, 3.5)
|
||||
outfile.save("output.pdf")
|
||||
Reference in New Issue
Block a user