[Python] pdf export with annotations from tkinter

Discussão em 'Python' iniciado por Stack, Junho 12, 2025.

  1. Stack

    Stack Membro Participativo

    Hi I am trying to create a program that allows a pdf to be annotated using .png and then exported but when I export the pdf the tick.png are all in the wrong locations to run the program you have to have a tick.png file in the same location as the .py file

    import tkinter as tk
    from tkinter import filedialog, messagebox
    from PIL import Image, ImageTk
    import fitz # PyMuPDF
    import os
    import json

    class PDFAnnotator:
    def __init__(self):
    self.root = tk.Tk()
    self.root.title("PDF Annotator")

    self.pdf = None
    self.page_index = 0
    self.annotations = [] # [page, tool, canvas_id, x_rel, y_rel]
    self.current_tool = None
    self.tick_count = 0

    self.undo_stack = []

    self.canvas_width = 600
    self.canvas_height = 800

    self.canvas = tk.Canvas(self.root, width=self.canvas_width, height=self.canvas_height, bg='grey')
    self.canvas.pack(side=tk.LEFT)
    self.canvas.bind("<Button-1>", self.on_click)
    self.canvas.bind("<B1-Motion>", self.on_drag)
    self.canvas.bind("<ButtonRelease-1>", self.on_drop)

    ctrl = tk.Frame(self.root)
    ctrl.pack(side=tk.RIGHT, fill=tk.Y)

    tk.Button(ctrl, text="Load PDF", command=self.load_pdf).pack(pady=5)
    tk.Button(ctrl, text="Prev Page", command=lambda: self.change_page(-1)).pack(pady=5)
    tk.Button(ctrl, text="Next Page", command=lambda: self.change_page(1)).pack(pady=5)

    self.icons = {}
    self.tk_icons = {}
    for name in ["tick", "cross", "noresponse", "TV", "BOD", "NDOB"]:
    path = f"{name}.png"
    if os.path.exists(path):
    img = Image.open(path).resize((30, 30)).convert("RGBA")
    img.save(path)
    self.tk_icons[name] = ImageTk.PhotoImage(img)
    self.icons[name] = path
    tk.Button(ctrl, text=name, command=lambda n=name: self.select_tool(n)).pack(pady=2)

    tk.Button(ctrl, text="Eraser", command=lambda: self.select_tool("eraser")).pack(pady=10)
    tk.Button(ctrl, text="Undo", command=self.undo).pack(pady=10)
    tk.Button(ctrl, text="Save Progress", command=self.save_progress).pack(pady=10)
    tk.Button(ctrl, text="Load Progress", command=self.load_progress).pack(pady=10)
    tk.Button(ctrl, text="Export PDF", command=self.export_pdf).pack(pady=15)

    self.tick_label = tk.Label(ctrl, text="Ticks: 0")
    self.tick_label.pack(pady=5)

    self.drag_data = {'item': None, 'dx': 0, 'dy': 0}
    self.page_render_info = {} # page_index: (img_width, img_height, x_offset, y_offset)

    self.root.mainloop()

    def load_pdf(self):
    path = filedialog.askopenfilename(filetypes=[("PDF", "*.pdf")])
    if not path:
    return
    self.pdf = fitz.open(path)
    self.page_index = 0
    self.annotations.clear()
    self.tick_count = 0
    self.tick_label.config(text="Ticks: 0")
    self.page_render_info.clear()
    self.render_page()

    def render_page(self):
    self.canvas.delete("all")
    if not self.pdf:
    return
    page = self.pdf[self.page_index]
    pix = page.get_pixmap(matrix=fitz.Matrix(2, 2))
    img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)

    img_ratio = img.width / img.height
    canvas_ratio = self.canvas_width / self.canvas_height

    if img_ratio > canvas_ratio:
    new_width = self.canvas_width
    new_height = int(self.canvas_width / img_ratio)
    x_offset = 0
    y_offset = (self.canvas_height - new_height) / 2
    else:
    new_height = self.canvas_height
    new_width = int(self.canvas_height * img_ratio)
    x_offset = (self.canvas_width - new_width) / 2
    y_offset = 0

    img = img.resize((new_width, new_height), Image.LANCZOS)
    self.tk_background = ImageTk.PhotoImage(img)
    self.canvas.create_image(x_offset, y_offset, anchor=tk.NW, image=self.tk_background)

    self.page_render_info[self.page_index] = (new_width, new_height, x_offset, y_offset)

    for ann in self.annotations:
    if ann[0] == self.page_index:
    x, y = ann[3] * self.canvas_width, ann[4] * self.canvas_height
    canvas_id = self.canvas.create_image(x, y, image=self.tk_icons[ann[1]])
    ann[2] = canvas_id

    def change_page(self, delta):
    if not self.pdf:
    return
    new = self.page_index + delta
    if 0 <= new < len(self.pdf):
    self.page_index = new
    self.render_page()

    def select_tool(self, name):
    self.current_tool = name

    def on_click(self, ev):
    if self.current_tool == "eraser":
    item = self.canvas.find_closest(ev.x, ev.y)[0]
    for ann in self.annotations:
    if ann[0] == self.page_index and ann[2] == item:
    if ann[1] == "tick":
    self.tick_count -= 1
    self.tick_label.config(text=f"Ticks: {self.tick_count}")
    self.undo_stack.append(('delete', ann.copy()))
    self.canvas.delete(item)
    self.annotations.remove(ann)
    break
    elif self.current_tool:
    self.place_annotation(ev.x, ev.y, self.current_tool)
    else:
    item = self.canvas.find_closest(ev.x, ev.y)
    if item:
    self.drag_data['item'] = item[0]
    self.drag_data['dx'] = ev.x
    self.drag_data['dy'] = ev.y

    def place_annotation(self, x, y, tool):
    tk_img = self.tk_icons[tool]
    canvas_id = self.canvas.create_image(x, y, image=tk_img)
    ann = [self.page_index, tool, canvas_id, x / self.canvas_width, y / self.canvas_height]
    self.annotations.append(ann)
    self.undo_stack.append(('add', ann.copy()))
    if tool == "tick":
    self.tick_count += 1
    self.tick_label.config(text=f"Ticks: {self.tick_count}")

    def on_drag(self, ev):
    item = self.drag_data['item']
    if item:
    dx = ev.x - self.drag_data['dx']
    dy = ev.y - self.drag_data['dy']
    self.canvas.move(item, dx, dy)
    self.drag_data['dx'] = ev.x
    self.drag_data['dy'] = ev.y

    def on_drop(self, ev):
    item = self.drag_data['item']
    if item:
    coords = self.canvas.coords(item)
    for ann in self.annotations:
    if ann[0] == self.page_index and ann[2] == item:
    ann[3] = coords[0] / self.canvas_width
    ann[4] = coords[1] / self.canvas_height
    self.undo_stack.append(('move', ann.copy()))
    break
    self.drag_data['item'] = None

    def undo(self):
    if not self.undo_stack:
    return
    action, ann = self.undo_stack.pop()
    if action == 'add':
    if ann[1] == "tick":
    self.tick_count -= 1
    self.tick_label.config(text=f"Ticks: {self.tick_count}")
    self.canvas.delete(ann[2])
    self.annotations.remove(ann)
    elif action == 'delete':
    tk_img = self.tk_icons[ann[1]]
    canvas_id = self.canvas.create_image(ann[3]*self.canvas_width, ann[4]*self.canvas_height, image=tk_img)
    ann[2] = canvas_id
    self.annotations.append(ann)
    if ann[1] == "tick":
    self.tick_count += 1
    self.tick_label.config(text=f"Ticks: {self.tick_count}")
    elif action == 'move':
    for a in self.annotations:
    if a[2] == ann[2]:
    a[3], a[4] = ann[3], ann[4]
    self.canvas.coords(ann[2], ann[3]*self.canvas_width, ann[4]*self.canvas_height)
    break

    def save_progress(self):
    file = filedialog.asksaveasfilename(defaultextension=".json", filetypes=[("JSON files", "*.json")])
    if not file:
    return
    data = {
    "annotations": [
    [page, tool, x_rel, y_rel] for page, tool, _, x_rel, y_rel in self.annotations
    ],
    "tick_count": self.tick_count
    }
    with open(file, "w") as f:
    json.dump(data, f)
    messagebox.showinfo("Save", "Progress saved!")

    def load_progress(self):
    file = filedialog.askopenfilename(filetypes=[("JSON files", "*.json")])
    if not file:
    return
    with open(file, "r") as f:
    data = json.load(f)
    self.annotations.clear()
    self.tick_count = data.get("tick_count", 0)
    self.tick_label.config(text=f"Ticks: {self.tick_count}")
    for page, tool, x_rel, y_rel in data.get("annotations", []):
    if tool not in self.tk_icons:
    continue
    if page == self.page_index:
    canvas_id = self.canvas.create_image(x_rel*self.canvas_width, y_rel*self.canvas_height, image=self.tk_icons[tool])
    else:
    canvas_id = None
    self.annotations.append([page, tool, canvas_id, x_rel, y_rel])
    self.render_page()
    messagebox.showinfo("Load", "Progress loaded!")

    def export_pdf(self):
    if not self.pdf:
    messagebox.showwarning("Export", "No PDF loaded")
    return
    save = filedialog.asksaveasfilename(defaultextension=".pdf", filetypes=[("PDF", "*.pdf")])
    if not save:
    return

    for page_idx, tool, _, x_rel, y_rel in self.annotations:
    page = self.pdf[page_idx]
    rect = page.rect

    img_width, img_height, x_offset, y_offset = self.page_render_info.get(page_idx, (self.canvas_width, self.canvas_height, 0, 0))

    x_canvas = x_rel * self.canvas_width
    y_canvas = y_rel * self.canvas_height
    y_canvas_flipped = self.canvas_height - y_canvas

    x_img = x_canvas - x_offset
    y_img = y_canvas_flipped - y_offset

    x_img_rel = x_img / img_width
    y_img_rel = y_img / img_height
    x_img_rel = max(0, min(1, x_img_rel))
    y_img_rel = max(0, min(1, y_img_rel))

    x_pdf = x_img_rel * rect.width
    y_pdf = y_img_rel * rect.height

    pix = fitz.Pixmap(self.icons[tool])
    icon_size = 20
    page.insert_image(fitz.Rect(x_pdf, y_pdf - icon_size, x_pdf + icon_size, y_pdf), pixmap=pix)

    last = self.pdf[-1]
    last.insert_text((50, 50), f"Total Score (Ticks): {self.tick_count}", fontsize=20, color=(0, 0, 1))
    self.pdf.save(save)
    messagebox.showinfo("Export", f"Saved to:\n{save}")

    if __name__ == "__main__":
    PDFAnnotator()

    Continue reading...

Compartilhe esta Página