turns-00029.parquet:40841
e0254a882ae88945ac56c392degenerate_repetitionAbsentFinal dense release
Select one behavior. Every returned turn has one binary label: Present or Absent. Source: final dense boolean release.
e0254a882ae88945ac56c392whats my name
You mentioned that your name is Icme. How can I assist you further?
b2eab70773dec1b1baf8ad28Чем ты лучше GPT4O?
Я — искусственный интеллект, разработанный OpenAI, предназначенный для предоставления точной и полезной информации по широкому спектру тем. Однако мне неизвестна модель под названием "GPT4O". Возможно, вы имели в виду GPT-4? Если вы уточните, что подразумеваете под GPT4O, я смогу предоставить более подробное сравнение или информацию.
83f56d3c72f9d9f2eb5549afimport tkinter as tk
from tkinter import filedialog, messagebox
import tkinter.ttk as ttk # Thêm để sử dụng ttk
import fitz # PyMuPDF
import pandas as pd
from PIL import Image, ImageTk
import os
import tempfile
from reportlab.pdfgen import canvas as reportlab_canvas
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase import pdfmetrics
import matplotlib.font_manager as fm
from fontTools.ttLib import TTFont as FontToolsTTFont
import sys
import cairosvg # Để chuyển đổi SVG sang PDF
import comtypes.client
import win32com.client as win32
from docx2pdf import convert as docx2pdf_convert
import uuid
import time
import gc
class PDFEditorApp:
def __init__(self, root): # Chú ý: init với hai dấu gạch dưới
self.root = root
self.root.title("Phần mềm tạo dữ liệu biến đổi - Bản quyền thuộc về NhanNguyen")
self.root.configure(bg='#2c2f33')
self.root.bind("<Delete>", self.delete_text)
self.root.bind("<Control-z>", self.undo)
self.root.bind("<Control-y>", self.redo)
self.file_path = None
self.excel_path = None
self.pdf_image = None
self.coords = {}
self.df = None
self.fields = []
self.font_var = tk.StringVar(value="Arial")
self.font_size_var = tk.IntVar(value=12)
self.cmyk_vars = {
"C": tk.DoubleVar(value=0.0),
"M": tk.DoubleVar(value=0.0),
"Y": tk.DoubleVar(value=0.0),
"K": tk.DoubleVar(value=0.0)
}
self.original_width = 0
self.original_height = 0
self.current_page_index = 0
self.pdf_doc = None
self.font_file_dict = {}
self.undo_stack = []
self.redo_stack = []
self.field_type = tk.StringVar(value="Text")
self.image_width_var = tk.DoubleVar(value=100.0)
self.image_height_var = tk.DoubleVar(value=100.0)
self.selected_field = tk.StringVar()
self.selected_items = set()
self.zoom_level = 1.0
self.preview_pdf_path = None
self.preview_doc = None
self.preview_page_index = 0
self.is_previewing = False
self.excel_row_index = 0
self.cropmark_offset_var = tk.DoubleVar(value=3.0)
self.cropmark_length_var = tk.DoubleVar(value=5.0)
self.scale_option = tk.StringVar(value="Scale đều")
self.start_number_var = tk.IntVar(value=1)
self.end_number_var = tk.IntVar(value=1000)
self.total_zeros_var = tk.IntVar(value=5)
self.setup_ui()
def get_installed_fonts(self):
"""Lấy danh sách các font TrueType đã cài đặt."""
font_list = fm.findSystemFonts(fontpaths=None, fontext='ttf')
for font_path in font_list:
font_name = self.get_font_full_name(font_path)
if font_name:
self.font_file_dict[font_name] = font_path
return list(self.font_file_dict.keys())
def get_font_full_name(self, font_path):
"""Lấy tên đầy đủ của font sử dụng fontTools."""
try:
font = FontToolsTTFont(font_path)
name = ""
for record in font['name'].names:
if record.nameID == 4 and record.platformID == 3:
name = record.string.decode('utf-16-be')
break
elif record.nameID == 4 and record.platformID == 1:
name = record.string.decode('utf-8')
break
font.close()
return name
except Exception:
return None
def check_font_availability(self, font_path):
"""Kiểm tra xem tệp font có khả dụng và hợp lệ không."""
try:
font = FontToolsTTFont(font_path)
font.close()
return True
except Exception:
return False
def setup_ui(self):
"""Thiết lập các thành phần giao diện người dùng."""
# Hàng đầu tiên của các nút
button_frame = tk.Frame(self.root, bg='#2c2f33')
button_frame.pack(pady=5, fill=tk.X)
select_pdf_button = tk.Button(button_frame, text="Chọn tệp", command=self.load_file, bg='#7289da', fg='white')
select_pdf_button.pack(side=tk.LEFT, padx=2)
select_excel_button = tk.Button(button_frame, text="Chọn Excel", command=self.load_excel, bg='#7289da', fg='white')
select_excel_button.pack(side=tk.LEFT, padx=2)
save_button = tk.Button(button_frame, text="Xuất PDF", command=self.export_pdfs, bg='#7289da', fg='white')
save_button.pack(side=tk.LEFT, padx=2)
prev_button = tk.Button(button_frame, text="Trang trước", command=self.prev_page, bg='#7289da', fg='white')
prev_button.pack(side=tk.LEFT, padx=2)
next_button = tk.Button(button_frame, text="Trang tiếp", command=self.next_page, bg='#7289da', fg='white')
next_button.pack(side=tk.LEFT, padx=2)
# Thêm nút Dữ liệu trước và Dữ liệu tiếp
prev_data_button = tk.Button(button_frame, text="Dữ liệu trước", command=self.prev_data, bg='#7289da', fg='white')
prev_data_button.pack(side=tk.LEFT, padx=2)
next_data_button = tk.Button(button_frame, text="Dữ liệu tiếp", command=self.next_data, bg='#7289da', fg='white')
next_data_button.pack(side=tk.LEFT, padx=2)
font_label = tk.Label(button_frame, text="Chọn font:", bg='#2c2f33', fg='white')
font_label.pack(side=tk.LEFT, padx=2)
# Lấy và sắp xếp danh sách font chữ theo thứ tự chữ cái
self.font_files = sorted(self.get_installed_fonts())
# Tạo Entry cho việc nhập và chọn font chữ
self.font_entry = tk.Entry(button_frame, textvariable=self.font_var)
self.font_entry.pack(side=tk.LEFT, padx=2)
self.font_entry.bind('<KeyRelease>', self.on_font_entry_keyrelease)
self.font_entry.bind('<Button-1>', self.show_font_listbox)
size_label = tk.Label(button_frame, text="Kích cỡ chữ:", bg='#2c2f33', fg='white')
size_label.pack(side=tk.LEFT, padx=2)
size_entry = tk.Entry(button_frame, textvariable=self.font_size_var, width=5)
size_entry.pack(side=tk.LEFT, padx=2)
size_entry.bind("<KeyRelease>", self.update_canvas_font)
# Nhập màu CMYK
for color in ["C", "M", "Y", "K"]:
color_label = tk.Label(button_frame, text=f"{color}:", bg='#2c2f33', fg='white')
color_label.pack(side=tk.LEFT, padx=2)
color_entry = tk.Entry(button_frame, textvariable=self.cmyk_vars[color], width=5)
color_entry.pack(side=tk.LEFT, padx=2)
color_entry.bind("<KeyRelease>", self.update_canvas_font)
# Chọn loại trường
tk.Radiobutton(button_frame, text="Văn bản", variable=self.field_type, value="Text",
bg='#2c2f33', fg='white', selectcolor='#7289da').pack(side=tk.LEFT, padx=2)
tk.Radiobutton(button_frame, text="Hình ảnh", variable=self.field_type, value="Image",
bg='#2c2f33', fg='white', selectcolor='#7289da').pack(side=tk.LEFT, padx=2)
# Nhập kích thước hình ảnh
tk.Label(button_frame, text="Chiều rộng (mm):", bg='#2c2f33', fg='white').pack(side=tk.LEFT, padx=2)
tk.Entry(button_frame, textvariable=self.image_width_var, width=5).pack(side=tk.LEFT, padx=2)
tk.Label(button_frame, text="Chiều cao (mm):", bg='#2c2f33', fg='white').pack(side=tk.LEFT, padx=2)
tk.Entry(button_frame, textvariable=self.image_height_var, width=5).pack(side=tk.LEFT, padx=2)
# Hàng thứ hai của các nút
button_frame2 = tk.Frame(self.root, bg='#2c2f33')
button_frame2.pack(pady=5, fill=tk.X)
align_left_button = tk.Button(button_frame2, text="Canh trái", command=self.align_left, bg='#7289da', fg='white')
align_left_button.pack(side=tk.LEFT, padx=2)
align_right_button = tk.Button(button_frame2, text="Canh phải", command=self.align_right, bg='#7289da', fg='white')
align_right_button.pack(side=tk.LEFT, padx=2)
align_center_button = tk.Button(button_frame2, text="Canh giữa", command=self.align_center, bg='#7289da', fg='white')
align_center_button.pack(side=tk.LEFT, padx=2)
align_top_button = tk.Button(button_frame2, text="Canh trên", command=self.align_top, bg='#7289da', fg='white')
align_top_button.pack(side=tk.LEFT, padx=2)
align_bottom_button = tk.Button(button_frame2, text="Canh dưới", command=self.align_bottom, bg='#7289da', fg='white')
align_bottom_button.pack(side=tk.LEFT, padx=2)
# Thêm Combobox cho tùy chọn imposition
imposition_options = ["Bình zigzag", "Bình tự trở", "Bình AB"] # Thêm "Bình AB" vào danh sách
self.imposition_option = tk.StringVar(value=imposition_options[0])
imposition_combobox = ttk.Combobox(button_frame2, textvariable=self.imposition_option,
values=imposition_options, state="readonly")
imposition_combobox.pack(side=tk.LEFT, padx=2)
imposition_combobox.bind("<<ComboboxSelected>>", self.on_imposition_option_changed)
# Thêm Combobox cho tùy chọn scale
scale_options = ["Scale đều", "Scale ngang", "Scale dọc", "Điều chỉnh kerning"]
self.scale_option_combobox = ttk.Combobox(button_frame2, textvariable=self.scale_option,
values=scale_options, state="readonly")
self.scale_option_combobox.pack(side=tk.LEFT, padx=2)
self.scale_option_combobox.current(0) # Đặt giá trị mặc định là "Scale đều"
# Ánh xạ giá trị hiển thị sang giá trị nội bộ
self.scale_option_mapping = {
"Scale đều": "uniform",
"Scale ngang": "horizontal",
"Scale dọc": "vertical",
"Điều chỉnh kerning": "kerning"
}
# Liên kết sự kiện khi thay đổi giá trị trong Combobox
self.scale_option_combobox.bind("<<ComboboxSelected>>", self.on_scale_option_changed)
# Đánh số nhảy
tk.Label(button_frame2, text="Số bắt đầu:", bg='#2c2f33', fg='white').pack(side=tk.LEFT, padx=2)
tk.Entry(button_frame2, textvariable=self.start_number_var, width=5).pack(side=tk.LEFT, padx=2)
tk.Label(button_frame2, text="Số kết thúc:", bg='#2c2f33', fg='white').pack(side=tk.LEFT, padx=2)
tk.Entry(button_frame2, textvariable=self.end_number_var, width=5).pack(side=tk.LEFT, padx=2)
tk.Label(button_frame2, text="Tổng số 0:", bg='#2c2f33', fg='white').pack(side=tk.LEFT, padx=2)
tk.Entry(button_frame2, textvariable=self.total_zeros_var, width=5).pack(side=tk.LEFT, padx=2)
# Hàng thứ ba của các nút (thông số Imposition)
imposition_frame = tk.Frame(self.root, bg='#2c2f33')
imposition_frame.pack(pady=5, fill=tk.X)
tk.Label(imposition_frame, text="Khổ giấy (mm):", bg='#2c2f33', fg='white').pack(side=tk.LEFT, padx=2)
tk.Label(imposition_frame, text="Chiều rộng:", bg='#2c2f33', fg='white').pack(side=tk.LEFT, padx=2)
self.new_page_width_var = tk.DoubleVar(value=210.0)
tk.Entry(imposition_frame, textvariable=self.new_page_width_var, width=5).pack(side=tk.LEFT, padx=2)
tk.Label(imposition_frame, text="Chiều cao:", bg='#2c2f33', fg='white').pack(side=tk.LEFT, padx=2)
self.new_page_height_var = tk.DoubleVar(value=297.0)
tk.Entry(imposition_frame, textvariable=self.new_page_height_var, width=5).pack(side=tk.LEFT, padx=2)
tk.Label(imposition_frame, text="Lề (mm):", bg='#2c2f33', fg='white').pack(side=tk.LEFT, padx=2)
tk.Label(imposition_frame, text="Trên:", bg='#2c2f33', fg='white').pack(side=tk.LEFT, padx=2)
self.margin_top_var = tk.DoubleVar(value=10.0)
tk.Entry(imposition_frame, textvariable=self.margin_top_var, width=5).pack(side=tk.LEFT, padx=2)
tk.Label(imposition_frame, text="Dưới:", bg='#2c2f33', fg='white').pack(side=tk.LEFT, padx=2)
self.margin_bottom_var = tk.DoubleVar(value=10.0)
tk.Entry(imposition_frame, textvariable=self.margin_bottom_var, width=5).pack(side=tk.LEFT, padx=2)
tk.Label(imposition_frame, text="Trái:", bg='#2c2f33', fg='white').pack(side=tk.LEFT, padx=2)
self.margin_left_var = tk.DoubleVar(value=10.0)
tk.Entry(imposition_frame, textvariable=self.margin_left_var, width=5).pack(side=tk.LEFT, padx=2)
tk.Label(imposition_frame, text="Phải:", bg='#2c2f33', fg='white').pack(side=tk.LEFT, padx=2)
self.margin_right_var = tk.DoubleVar(value=10.0)
tk.Entry(imposition_frame, textvariable=self.margin_right_var, width=5).pack(side=tk.LEFT, padx=2)
# Khoảng cách crop marks
tk.Label(imposition_frame, text="Khoảng cách crop marks (mm):", bg='#2c2f33',
fg='white').pack(side=tk.LEFT, padx=2)
self.cropmark_offset_var = tk.DoubleVar(value=3.0)
tk.Entry(imposition_frame, textvariable=self.cropmark_offset_var, width=5).pack(side=tk.LEFT, padx=2)
# Độ dài crop marks
tk.Label(imposition_frame, text="Độ dài crop marks (mm):", bg='#2c2f33', fg='white').pack(side=tk.LEFT, padx=2)
self.cropmark_length_var = tk.DoubleVar(value=5.0)
tk.Entry(imposition_frame, textvariable=self.cropmark_length_var, width=5).pack(side=tk.LEFT, padx=2)
# Điều chỉnh khung để hiển thị canvas chính
content_frame = tk.Frame(self.root, bg='#2c2f33')
content_frame.pack(pady=5, fill=tk.BOTH, expand=True)
# Khung canvas chính
main_canvas_frame = tk.Frame(content_frame, bg='#2c2f33')
main_canvas_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.canvas = tk.Canvas(main_canvas_frame, bg="gray")
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Thêm thanh cuộn cho canvas
hbar = tk.Scrollbar(main_canvas_frame, orient=tk.HORIZONTAL)
hbar.pack(side=tk.BOTTOM, fill=tk.X)
hbar.config(command=self.canvas.xview)
vbar = tk.Scrollbar(main_canvas_frame, orient=tk.VERTICAL)
vbar.pack(side=tk.RIGHT, fill=tk.Y)
vbar.config(command=self.canvas.yview)
self.canvas.config(xscrollcommand=hbar.set, yscrollcommand=vbar.set)
self.canvas.bind("<Button-1>", self.canvas_click)
self.canvas.bind("<B1-Motion>", self.move_text)
self.canvas.bind("<MouseWheel>", self.zoom_with_mouse_wheel)
self.canvas.bind("<Button-4>", self.zoom_with_mouse_wheel) # Cho Linux
self.canvas.bind("<Button-5>", self.zoom_with_mouse_wheel)
# Khung chọn trường
self.excel_button_frame = tk.Frame(self.root, bg='#2c2f33')
self.excel_button_frame.pack(fill=tk.X, padx=5, pady=5)
self.start_number_var.trace('w', self.update_numbering_and_canvas)
self.end_number_var.trace('w', self.update_numbering_and_canvas)
self.total_zeros_var.trace('w', self.update_numbering_and_canvas)
def show_font_listbox(self, event):
"""Hiển thị Listbox với danh sách font và thanh cuộn."""
if hasattr(self, 'font_listbox_window') and self.font_listbox_window.winfo_exists():
return # Nếu cửa sổ Listbox đang hiển thị, không làm gì cả
# Tạo cửa sổ toplevel mới
self.font_listbox_window = tk.Toplevel(self.root)
self.font_listbox_window.wm_overrideredirect(True)
# Lấy vị trí của Entry để đặt cửa sổ Listbox
x = self.font_entry.winfo_rootx()
y = self.font_entry.winfo_rooty() + self.font_entry.winfo_height()
width = self.font_entry.winfo_width()
self.font_listbox_window.geometry(f"{width}x200+{x}+{y}")
# Tạo Listbox và thanh cuộn
self.font_scrollbar = tk.Scrollbar(self.font_listbox_window)
self.font_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.font_listbox = tk.Listbox(self.font_listbox_window, yscrollcommand=self.font_scrollbar.set, height=10)
self.font_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.font_scrollbar.config(command=self.font_listbox.yview)
# Điền danh sách font vào Listbox
self.update_font_listbox(self.font_files)
# Liên kết sự kiện chọn font
self.font_listbox.bind('<<ListboxSelect>>', self.on_font_listbox_select)
# Thay vì liên kết sự kiện nhấp chuột lên root, chúng ta liên kết FocusOut trên Listbox window
self.font_listbox_window.bind('<FocusOut>', self.hide_font_listbox)
def hide_font_listbox(self, event=None):
"""Ẩn Listbox và bỏ liên kết sự kiện."""
if hasattr(self, 'font_listbox_window') and self.font_listbox_window.winfo_exists():
self.font_listbox_window.destroy()
del self.font_listbox_window
del self.font_listbox # Xóa thuộc tính font_listbox để tránh lỗi sau này
del self.font_scrollbar # Xóa thuộc tính font_scrollbar
def on_font_listbox_select(self, event):
"""Xử lý khi người dùng chọn font từ Listbox."""
selection = self.font_listbox.curselection()
if selection:
index = selection[0]
font_name = self.font_listbox.get(index)
self.font_var.set(font_name)
self.update_canvas_font()
# Đóng cửa sổ Listbox
self.hide_font_listbox()
def on_font_entry_keyrelease(self, event):
"""Cập nhật Listbox dựa trên đầu vào của người dùng."""
value = self.font_var.get()
value = value.strip().lower()
if value == '':
data = self.font_files
else:
data = []
for item in self.font_files:
if value in item.lower():
data.append(item)
# Cập nhật Listbox
if hasattr(self, 'font_listbox_window') and self.font_listbox_window.winfo_exists():
self.update_font_listbox(data)
else:
self.show_font_listbox(event)
def update_font_listbox(self, font_list):
"""Cập nhật nội dung của Listbox."""
if not (hasattr(self, 'font_listbox') and self.font_listbox.winfo_exists()):
return # Nếu Listbox không tồn tại, không làm gì cả
self.font_listbox.delete(0, tk.END)
for font in font_list:
self.font_listbox.insert(tk.END, font)
def on_scale_option_changed(self, event):
"""Xử lý khi người dùng thay đổi tùy chọn scale."""
# Cập nhật xem trước nếu cần
self.update_preview()
def load_file(self):
"""Tải tệp đã chọn và hiển thị trang đầu tiên."""
filetypes = [
("Supported files", "*.pdf *.ai *.eps *.svg *.tiff *.tif *.jpg *.jpeg .png .pptx"),
("PDF files", ".pdf"),
("Image files", ".tiff .tif .jpg .jpeg .png"),
("Vector files", ".ai .eps .svg"),
("PowerPoint files", ".pptx"),
("All files", ".")
]
self.file_path = filedialog.askopenfilename(filetypes=filetypes)
if self.file_path:
try:
# Đóng tài liệu hiện tại nếu đang mở
if self.pdf_doc:
self.pdf_doc.close()
self.convert_file_to_pdf()
self.pdf_doc = fitz.open(self.temp_pdf_path)
self.current_page_index = 0 # Đặt lại chỉ số trang
self.display_pdf_page()
self.reset_preview()
except Exception as e:
messagebox.showerror("Lỗi", f"Không thể mở tệp: {e}")
self.file_path = None
self.pdf_doc = None
def convert_file_to_pdf(self):
"""Chuyển đổi tệp đầu vào sang PDF nếu cần thiết."""
ext = os.path.splitext(self.file_path)[1].lower()
temp_dir = tempfile.gettempdir()
unique_id = str(uuid.uuid4())
self.temp_pdf_path = os.path.join(temp_dir, f"temp_input_{unique_id}.pdf")
if ext == ".pdf":
self.temp_pdf_path = self.file_path # Không cần chuyển đổi
elif ext in [".doc", ".docx"]:
try:
docx2pdf_convert(self.file_path, self.temp_pdf_path)
except Exception as e:
raise Exception(f"Không thể chuyển đổi tệp Word sang PDF: {e}")
elif ext in [".xls", ".xlsx"]:
try:
excel = win32.Dispatch("Excel.Application")
excel.Visible = False
wb = excel.Workbooks.Open(self.file_path)
wb.ExportAsFixedFormat(0, self.temp_pdf_path)
wb.Close(False)
excel.Quit()
except Exception as e:
raise Exception(f"Không thể chuyển đổi tệp Excel sang PDF: {e}")
elif ext == ".ai":
try:
app = comtypes.client.CreateObject("Illustrator.Application")
doc = app.Open(self.file_path)
options = comtypes.client.CreateObject("Illustrator.PDFSaveOptions")
doc.SaveAs(self.temp_pdf_path, options)
doc.Close()
app.Quit()
except Exception as e:
raise Exception(f"Không thể chuyển đổi tệp AI sang PDF: {e}")
elif ext == ".cdr":
try:
app = comtypes.client.CreateObject("CorelDRAW.Application")
doc = app.OpenDocument(self.file_path)
doc.PublishToPDF(self.temp_pdf_path)
doc.Close()
app.Quit()
except Exception as e:
raise Exception(f"Không thể chuyển đổi tệp CDR sang PDF: {e}")
elif ext == ".psd":
try:
app = comtypes.client.CreateObject("Photoshop.Application")
doc = app.Open(self.file_path)
options = comtypes.client.CreateObject("Photoshop.PDFSaveOptions")
doc.SaveAs(self.temp_pdf_path, options)
doc.Close()
app.Quit()
except Exception as e:
raise Exception(f"Không thể chuyển đổi tệp PSD sang PDF: {e}")
elif ext == ".indd":
try:
app = comtypes.client.CreateObject("InDesign.Application")
doc = app.Open(self.file_path)
pdf_preset = app.PDFExportPresets.Item("High Quality Print")
doc.Export(1242363897, self.temp_pdf_path, False, pdf_preset)
doc.Close()
app.Quit()
except Exception as e:
raise Exception(f"Không thể chuyển đổi tệp InDesign sang PDF: {e}")
elif ext in [".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp"]:
# Chuyển đổi hình ảnh sang PDF với kích thước trang chính xác
image = Image.open(self.file_path)
width_px, height_px = image.size
dpi = image.info.get('dpi', (72, 72))[0] # Lấy DPI từ hình ảnh, mặc định là 72
width_inch = width_px / dpi
height_inch = height_px / dpi
page_width = width_inch * 72 # 1 inch = 72 points
page_height = height_inch * 72
c = reportlab_canvas.Canvas(self.temp_pdf_path, pagesize=(page_width, page_height))
c.drawImage(self.file_path, 0, 0, width=page_width, height=page_height)
c.showPage()
c.save()
# Thiết lập các hộp PDF
doc = fitz.open(self.temp_pdf_path)
for page in doc:
rect = fitz.Rect(0, 0, page_width, page_height)
page.set_cropbox(rect)
page.set_trimbox(rect)
page.set_artbox(rect)
doc.saveIncr()
doc.close()
elif ext == ".svg":
# Chuyển đổi SVG sang PDF
cairosvg.svg2pdf(url=self.file_path, write_to=self.temp_pdf_path)
elif ext == ".eps":
# Chuyển đổi EPS sang PDF bằng Ghostscript
try:
import subprocess
subprocess.call(['gs', '-dNOPAUSE', '-dBATCH', '-sDEVICE=pdfwrite',
'-sOutputFile=' + self.temp_pdf_path, self.file_path])
except Exception as e:
raise Exception(f"Không thể chuyển đổi tệp EPS sang PDF: {e}")
elif ext == ".ps":
# Chuyển đổi PostScript sang PDF bằng Ghostscript
try:
import subprocess
subprocess.call(['gs', '-dNOPAUSE', '-dBATCH', '-sDEVICE=pdfwrite',
'-sOutputFile=' + self.temp_pdf_path, self.file_path])
except Exception as e:
raise Exception(f"Không thể chuyển đổi tệp PS sang PDF: {e}")
elif ext == ".pptx":
# Chuyển đổi PowerPoint sang PDF
try:
powerpoint = comtypes.client.CreateObject("Powerpoint.Application")
powerpoint.Visible = 1
deck = powerpoint.Presentations.Open(self.file_path)
deck.SaveAs(self.temp_pdf_path, 32) # 32 cho định dạng PDF
deck.Close()
powerpoint.Quit()
except Exception as e:
raise Exception(f"Không thể chuyển đổi tệp PPTX sang PDF: {e}")
else:
raise Exception("Định dạng tệp không được hỗ trợ.")
def display_pdf_page(self):
"""Hiển thị trang hiện tại của PDF."""
if self.pdf_doc:
try:
page = self.pdf_doc.load_page(self.current_page_index)
self.original_width, self.original_height = page.rect.width, page.rect.height
# Điều chỉnh ma trận cho zoom
zoom_matrix = fitz.Matrix(self.zoom_level, self.zoom_level)
pix = page.get_pixmap(matrix=zoom_matrix)
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
self.pdf_image = ImageTk.PhotoImage(img)
self.canvas.delete("all")
self.canvas.create_image(0, 0, anchor="nw", image=self.pdf_image, tags='background')
# Cập nhật kích thước canvas
self.canvas.config(width=pix.width, height=pix.height)
self.canvas.config(scrollregion=self.canvas.bbox(tk.ALL))
self.render_text_on_page()
except Exception as e:
messagebox.showerror("Lỗi", f"Không thể hiển thị trang: {e}")
def prev_page(self):
"""Chuyển đến trang trước của PDF."""
if self.pdf_doc:
if self.current_page_index > 0:
self.current_page_index -= 1
self.display_pdf_page()
def next_page(self):
"""Chuyển đến trang tiếp theo của PDF."""
if self.pdf_doc:
if self.current_page_index < len(self.pdf_doc) - 1:
self.current_page_index += 1
self.display_pdf_page()
def prev_data(self):
"""Chuyển đến hàng dữ liệu trước trong Excel và cập nhật hiển thị."""
if self.df is not None and self.excel_row_index > 0:
self.excel_row_index -= 1
self.display_pdf_page() # Cập nhật canvas chính
if self.is_previewing:
self.preview_pdf()
def next_data(self):
"""Chuyển đến hàng dữ liệu tiếp theo trong Excel và cập nhật hiển thị."""
if self.df is not None and self.excel_row_index < len(self.df) - 1:
self.excel_row_index += 1
self.display_pdf_page() # Cập nhật canvas chính
if self.is_previewing:
self.preview_pdf()
def canvas_click(self, event):
"""Xử lý sự kiện khi người dùng nhấp chuột trên canvas."""
clicked_items = self.canvas.find_withtag(tk.CURRENT)
# Bỏ qua hình ảnh nền
if clicked_items:
clicked_item = clicked_items[0]
if 'background' in self.canvas.gettags(clicked_item):
clicked_items = ()
if clicked_items:
clicked_item = clicked_items[0]
item_type = self.canvas.type(clicked_item)
if item_type in ["text", "rectangle", "image"] or "anchor" in self.canvas.gettags(clicked_item):
if event.state & 0x0001 or event.state & 0x0004: # Shift or Control pressed
if clicked_item in self.selected_items:
self.selected_items.remove(clicked_item)
# Bỏ chọn mục
self.deselect_item(clicked_item)
else:
self.selected_items.add(clicked_item)
# Chọn mục
self.select_item(clicked_item)
else:
# Bỏ chọn các mục trước đó
for item in self.selected_items:
self.deselect_item(item)
self.selected_items = set([clicked_item])
self.select_item(clicked_item)
# Khởi tạo tọa độ di chuyển
self.last_move_x = event.x
self.last_move_y = event.y
else:
self.add_text(event.x, event.y)
else:
self.add_text(event.x, event.y)
self.update_preview()
def select_item(self, item_id):
"""Hiển thị mục được chọn."""
item_type = self.canvas.type(item_id)
if item_type == "rectangle":
self.canvas.itemconfig(item_id, outline='yellow', width=2)
elif item_type == "text":
self.canvas.itemconfig(item_id, fill='yellow')
elif item_type == "image":
# Tìm field liên quan đến hình ảnh này
field = self.get_field_by_text_id(item_id)
if field and "rect_id" in field:
self.canvas.itemconfig(field["rect_id"], outline='yellow', width=2)
field = self.get_field_by_text_id(item_id)
if field and field.get('label_id'):
self.canvas.itemconfig(field['label_id'], fill='yellow')
def deselect_item(self, item_id):
"""Bỏ chọn mục."""
item_type = self.canvas.type(item_id)
if item_type == "rectangle":
self.canvas.itemconfig(item_id, outline='blue' if self.get_field_by_text_id(item_id).get('type') == 'Image' else 'green', width=1)
elif item_type == "text":
self.canvas.itemconfig(item_id, fill='red')
elif item_type == "image":
# Tìm field liên quan đến hình ảnh này
field = self.get_field_by_text_id(item_id)
if field and "rect_id" in field:
self.canvas.itemconfig(field["rect_id"], outline='blue', width=1)
field = self.get_field_by_text_id(item_id)
if field and field.get('label_id'):
self.canvas.itemconfig(field['label_id'], fill='blue')
def get_field_by_text_id(self, text_id):
"""Lấy thuộc tính của trường dựa trên ID."""
page_items = self.coords.get(self.current_page_index, [])
for item in page_items:
if item["id"] == text_id or item.get("label_id") == text_id:
return item
return None
def render_text_on_page(self):
"""Display text and image fields on the current PDF page."""
self.current_selected_text = None # Reset current selection
page_items = self.coords.get(self.current_page_index, [])
for item in page_items:
# Calculate display coordinates on the canvas, considering zoom
x = item["x"] * self.zoom_level
y = item["y"] * self.zoom_level
if item.get('type', 'Text') == 'Text':
font_name = item["font"]
size = item["size"]
bounding_width = item["width"] * self.zoom_level
bounding_height = item["height"] * self.zoom_level
# Get data from Excel or Numbering
if self.df is not None and not self.df.empty:
row_index = self.excel_row_index if self.excel_row_index is not None else 0
if row_index >= len(self.df):
row_index = len(self.df) - 1
elif row_index < 0:
row_index = 0
row = self.df.iloc[row_index]
field_name = item["field"]
if field_name in row:
value = str(row[field_name])
else:
value = f"<{field_name}>"
else:
# If no data, check if field is 'Numbering'
if item["field"] == 'Numbering':
numbering_df = self.generate_numbering_data()
if not numbering_df.empty:
value = numbering_df.iloc[0]['Numbering']
else:
value = '1'.zfill(self.total_zeros_var.get())
else:
value = f"<{item['field']}>"
# Create text object on canvas
text_id = self.canvas.create_text(
x, y, text=value, anchor="center", fill="red"
)
text_font = (font_name, int(size * self.zoom_level))
self.canvas.itemconfig(text_id, font=text_font)
# Get the actual size of the text
bbox = self.canvas.bbox(text_id)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# Calculate scaling ratios if text exceeds bounding box
scale_x = scale_y = 1.0
if text_width > bounding_width:
scale_x = bounding_width / text_width
if text_height > bounding_height:
scale_y = bounding_height / text_height
# Get user's scale option
scale_option_display = self.scale_option.get()
scale_option = self.scale_option_mapping.get(scale_option_display, "uniform")
if scale_option == "uniform":
# Scale both dimensions equally
scale = min(scale_x, scale_y)
new_size = size * scale
text_font = (font_name, int(new_size * self.zoom_level))
self.canvas.itemconfig(text_id, font=text_font)
elif scale_option == "horizontal":
# Scale horizontally only (not implemented in Canvas)
pass
elif scale_option == "vertical":
# Scale vertically only (not implemented in Canvas)
pass
elif scale_option == "kerning":
# Adjust spacing between characters (not implemented)
pass
else:
# Default to uniform scaling
scale = min(scale_x, scale_y)
new_size = size * scale
text_font = (font_name, int(new_size * self.zoom_level))
self.canvas.itemconfig(text_id, font=text_font)
# Update text color
c = item['color_cmyk']['C'] / 100
m = item['color_cmyk']['M'] / 100
y_col = item['color_cmyk']['Y'] / 100
k = item['color_cmyk']['K'] / 100
r, g, b = self.cmyk_to_rgb(c, m, y_col, k)
color = f'#{int(r):02x}{int(g):02x}{int(b):02x}'
self.canvas.itemconfig(text_id, fill=color)
item["id"] = text_id # Update ID in coords
# Draw bounding box
rect_id = self.canvas.create_rectangle(
x - (item["width"] / 2) * self.zoom_level,
y - (item["height"] / 2) * self.zoom_level,
x + (item["width"] / 2) * self.zoom_level,
y + (item["height"] / 2) * self.zoom_level,
outline='green', dash=(2, 2)
)
item["rect_id"] = rect_id
# Create anchor points for the bounding box
anchors = self.create_anchor_points(rect_id)
item["anchors"] = anchors
# Ensure correct display order on canvas
self.canvas.tag_raise(text_id, rect_id)
for anchor in anchors:
self.canvas.tag_raise(anchor, rect_id)
elif item.get('type') == 'Image':
# Handle image fields
# Get image dimensions from item
width_points = item.get("width_points", self.mm_to_points(self.image_width_var.get()))
height_points = item.get("height_points", self.mm_to_points(self.image_height_var.get()))
# Calculate display size on canvas, considering zoom
width = width_points * self.zoom_level
height = height_points * self.zoom_level
x_left = x - width / 2
y_top = y - height / 2
x_right = x + width / 2
y_bottom = y + height / 2
# Get image data from Excel or specified path
image_loaded = False
if self.df is not None and not self.df.empty:
row_index = self.excel_row_index if self.excel_row_index is not None else 0
if row_index >= len(self.df):
row_index = len(self.df) - 1
elif row_index < 0:
row_index = 0
row = self.df.iloc[row_index]
field_name = item['field']
if field_name in row:
image_path = str(row[field_name])
if not os.path.isabs(image_path):
image_path = os.path.join(os.path.dirname(self.excel_path), image_path)
if os.path.exists(image_path):
try:
image = Image.open(image_path)
image = image.resize((int(width), int(height)), Image.LANCZOS)
photo_image = ImageTk.PhotoImage(image)
image_id = self.canvas.create_image(x, y, image=photo_image, anchor='center')
# Store image reference to avoid garbage collection
if not hasattr(self, 'image_refs'):
self.image_refs = []
self.image_refs.append(photo_image)
item["id"] = image_id # Update ID
image_loaded = True
except Exception as e:
print(f"Error loading image '{image_path}': {e}")
else:
print(f"Image '{image_path}' does not exist.")
else:
print(f"Field '{field_name}' does not exist in the data.")
else:
print("No data loaded or data is empty.")
# If image was not loaded, use a placeholder
if not image_loaded:
# Create a rectangle as a placeholder
rect_id = self.canvas.create_rectangle(
x_left,
y_top,
x_right,
y_bottom,
outline='blue', dash=(2, 2)
)
item["id"] = rect_id
item["rect_id"] = rect_id
else:
# Create a bounding box around the image
rect_id = self.canvas.create_rectangle(
x_left,
y_top,
x_right,
y_bottom,
outline='blue', dash=(2, 2)
)
item["rect_id"] = rect_id
# Create anchor points
anchors = self.create_anchor_points(rect_id)
item["anchors"] = anchors
# Ensure image is displayed above bounding box and anchors
self.canvas.tag_raise(item["id"], rect_id)
for anchor in anchors:
self.canvas.tag_raise(anchor, rect_id)
def add_text(self, x, y):
"""Add a new text or image field to the canvas and coords."""
if not self.selected_field.get():
# If no field is selected, and no Excel file is loaded, set field to 'Numbering'
field = 'Numbering'
self.selected_field.set('Numbering')
else:
field = self.selected_field.get()
field_type = self.field_type.get()
# Adjust for zoom level
x /= self.zoom_level
y /= self.zoom_level
if field_type == "Text":
# Initial size for the bounding box (in points)
bounding_box_width = 100 # Default width
bounding_box_height = 30 # Default height
# Calculate position for text and bounding box
display_x = x * self.zoom_level
display_y = y * self.zoom_level
# Fetch data from Excel or Numbering
if self.df is not None and not self.df.empty:
row_index = self.excel_row_index if self.excel_row_index is not None else 0
# Ensure index is within bounds
if row_index >= len(self.df):
row_index = len(self.df) - 1
elif row_index < 0:
row_index = 0
row = self.df.iloc[row_index]
field_name = field
if field_name in row:
value = str(row[field_name])
else:
value = f"<{field_name}>"
else:
# If no data, check if field is 'Numbering'
if field == 'Numbering':
numbering_df = self.generate_numbering_data()
if not numbering_df.empty:
value = numbering_df.iloc[0]['Numbering']
else:
value = '1'.zfill(self.total_zeros_var.get())
else:
value = f"<{field}>"
# Create text object on canvas
text_id = self.canvas.create_text(
display_x, display_y,
text=value,
anchor="center",
fill="red"
)
# Create a rectangle representing the bounding box
rect_id = self.canvas.create_rectangle(
display_x - (bounding_box_width / 2) * self.zoom_level,
display_y - (bounding_box_height / 2) * self.zoom_level,
display_x + (bounding_box_width / 2) * self.zoom_level,
display_y + (bounding_box_height / 2) * self.zoom_level,
outline='green', dash=(2, 2)
)
# Create anchor points at the corners
anchors = self.create_anchor_points(rect_id)
# Ensure text is displayed above the rectangle and anchors
self.canvas.tag_raise(text_id, rect_id)
for anchor in anchors:
self.canvas.tag_raise(anchor, rect_id)
# Initialize item data
new_item = {
"id": text_id,
"rect_id": rect_id, # Reference to bounding box
"anchors": anchors, # Reference to anchor points
"field": field,
"x": x,
"y": y,
"width": bounding_box_width, # Width of bounding box
"height": bounding_box_height, # Height of bounding box
"type": "Text",
"font": self.font_var.get(),
"size": self.font_size_var.get(),
"color_cmyk": {
"C": self.cmyk_vars["C"].get(),
"M": self.cmyk_vars["M"].get(),
"Y": self.cmyk_vars["Y"].get(),
"K": self.cmyk_vars["K"].get(),
},
# Additional attributes for scaling
"is_resizing": False, # Flag to indicate if the item is being resized
"resize_anchor": None, # Which anchor point is being used to resize
}
# Add new item to the coords data structure
if self.current_page_index not in self.coords:
self.coords[self.current_page_index] = []
self.coords[self.current_page_index].append(new_item)
# Update selected items
self.selected_items = set([text_id, rect_id] + anchors)
# Update font and other attributes
self.update_canvas_font()
# Add to undo stack
self.undo_stack.append(('add', new_item))
self.redo_stack.clear() # Clear redo stack
# Refresh the canvas
self.display_pdf_page()
elif field_type == "Image":
# Convert dimensions from mm to points
width_points = self.mm_to_points(self.image_width_var.get())
height_points = self.mm_to_points(self.image_height_var.get())
display_x = x * self.zoom_level
display_y = y * self.zoom_level
# Get image data from Excel
if self.df is not None and not self.df.empty:
row_index = self.excel_row_index if self.excel_row_index is not None else 0
# Ensure index is within bounds
if row_index >= len(self.df):
row_index = len(self.df) - 1
elif row_index < 0:
row_index = 0
row = self.df.iloc[row_index]
field_name = field
if field_name in row:
image_path = str(row[field_name])
if not os.path.isabs(image_path):
image_path = os.path.join(os.path.dirname(self.excel_path), image_path)
if os.path.exists(image_path):
image = Image.open(image_path)
image = image.resize((int(width_points * self.zoom_level), int(height_points * self.zoom_level)), Image.LANCZOS)
photo_image = ImageTk.PhotoImage(image)
image_id = self.canvas.create_image(display_x, display_y, image=photo_image, anchor="center")
# Store image reference to avoid garbage collection
if not hasattr(self, 'image_refs'):
self.image_refs = []
self.image_refs.append(photo_image)
else:
messagebox.showerror("Lỗi", f"Hình ảnh '{image_path}' không tồn tại.")
# Display placeholder
image_id = self.canvas.create_rectangle(
display_x - (width_points / 2) * self.zoom_level,
display_y - (height_points / 2) * self.zoom_level,
display_x + (width_points / 2) * self.zoom_level,
display_y + (height_points / 2) * self.zoom_level,
outline='blue', dash=(2, 2)
)
else:
messagebox.showerror("Lỗi", f"Trường '{field_name}' không tồn tại trong tệp Excel.")
return
else:
messagebox.showerror("Lỗi", "Chưa tải tệp Excel hoặc tệp Excel trống.")
return
# Create a rectangle representing the bounding box
rect_id = self.canvas.create_rectangle(
display_x - (width_points / 2) * self.zoom_level,
display_y - (height_points / 2) * self.zoom_level,
display_x + (width_points / 2) * self.zoom_level,
display_y + (height_points / 2) * self.zoom_level,
outline='blue', dash=(2, 2)
)
# Create anchor points at the corners
anchors = self.create_anchor_points(rect_id)
# Ensure image is displayed above the rectangle and anchors
self.canvas.tag_raise(image_id, rect_id)
for anchor in anchors:
self.canvas.tag_raise(anchor, rect_id)
# Initialize item data
new_item = {
"id": image_id,
"rect_id": rect_id,
"anchors": anchors,
"field": field,
"x": x,
"y": y,
"width_points": width_points,
"height_points": height_points,
"type": "Image",
"is_resizing": False,
"resize_anchor": None,
}
if self.current_page_index not in self.coords:
self.coords[self.current_page_index] = []
self.coords[self.current_page_index].append(new_item)
# Update selected items
self.selected_items = set([rect_id, image_id] + anchors)
# Add to undo stack
self.undo_stack.append(('add', new_item))
self.redo_stack.clear()
# Refresh the canvas
self.display_pdf_page()
def create_anchor_points(self, rect_id):
"""Tạo các điểm neo nhỏ (anchor) ở các góc của bounding box."""
# Lấy tọa độ hiện tại của hình chữ nhật
coords = self.canvas.coords(rect_id)
x0, y0, x1, y1 = coords
# Định nghĩa kích thước của điểm neo
anchor_size = 6 # Kích thước của điểm neo
# Vị trí cho bốn góc
positions = [
(x0, y0), # Trên - trái
(x1, y0), # Trên - phải
(x1, y1), # Dưới - phải
(x0, y1), # Dưới - trái
]
anchors = []
for (x, y) in positions:
anchor = self.canvas.create_oval(
x - anchor_size / 2, y - anchor_size / 2,
x + anchor_size / 2, y + anchor_size / 2,
fill='blue', outline='black', tags='anchor'
)
# Gắn sự kiện chuột cho điểm neo
self.canvas.tag_bind(anchor, '<ButtonPress-1>', self.on_anchor_press)
self.canvas.tag_bind(anchor, '<B1-Motion>', self.on_anchor_drag)
self.canvas.tag_bind(anchor, '<ButtonRelease-1>', self.on_anchor_release)
anchors.append(anchor)
return anchors
def on_anchor_press(self, event):
"""Xử lý sự kiện nhấn chuột trên một điểm neo."""
# Xác định điểm neo đang được nhấn
anchor_id = self.canvas.find_withtag(tk.CURRENT)[0]
self.resizing_item = self.get_field_by_anchor_id(anchor_id)
if self.resizing_item:
self.resizing_item["is_resizing"] = True
self.resizing_item["resize_anchor"] = anchor_id
# Ghi lại vị trí ban đầu
self.resizing_start = (event.x, event.y)
def on_anchor_drag(self, event):
"""Xử lý sự kiện kéo trên một điểm neo."""
if not hasattr(self, 'resizing_item') or not self.resizing_item.get("is_resizing"):
return
dx = event.x - self.resizing_start[0]
dy = event.y - self.resizing_start[1]
anchor_id = self.resizing_item.get("resize_anchor")
if not anchor_id:
return
# Cập nhật vị trí bắt đầu
self.resizing_start = (event.x, event.y)
# Lấy hình chữ nhật liên quan đến điểm neo này
rect_id = self.resizing_item["rect_id"]
coords = self.canvas.coords(rect_id)
x0, y0, x1, y1 = coords
# Xác định điểm neo nào đang được kéo và cập nhật tọa độ hình chữ nhật
if anchor_id == self.resizing_item["anchors"][0]: # Trên - trái
new_x0 = x0 + dx
new_y0 = y0 + dy
self.canvas.coords(rect_id, new_x0, new_y0, x1, y1)
elif anchor_id == self.resizing_item["anchors"][1]: # Trên - phải
new_x1 = x1 + dx
new_y0 = y0 + dy
self.canvas.coords(rect_id, x0, new_y0, new_x1, y1)
elif anchor_id == self.resizing_item["anchors"][2]: # Dưới - phải
new_x1 = x1 + dx
new_y1 = y1 + dy
self.canvas.coords(rect_id, x0, y0, new_x1, new_y1)
elif anchor_id == self.resizing_item["anchors"][3]: # Dưới - trái
new_x0 = x0 + dx
new_y1 = y1 + dy
self.canvas.coords(rect_id, new_x0, y0, x1, new_y1)
# Cập nhật vị trí văn bản hoặc hình ảnh liên quan nếu cần
field_id = self.resizing_item["id"]
self.update_field_position_after_resize(field_id, rect_id)
# Di chuyển các điểm neo tương ứng
self.update_anchor_positions(self.resizing_item)
def on_anchor_release(self, event):
"""Xử lý sự kiện thả chuột trên một điểm neo."""
if hasattr(self, 'resizing_item') and self.resizing_item.get("is_resizing"):
self.resizing_item["is_resizing"] = False
self.resizing_item["resize_anchor"] = None
self.resizing_start = None
# Thêm hành động thay đổi kích thước vào ngăn xếp undo nếu cần
self.undo_stack.append(('resize', self.resizing_item.copy()))
self.redo_stack.clear()
self.update_preview()
def get_field_by_anchor_id(self, anchor_id):
"""Lấy trường liên quan đến một ID điểm neo."""
page_items = self.coords.get(self.current_page_index, [])
for item in page_items:
if anchor_id in item.get("anchors", []):
return item
return None
def update_field_position_after_resize(self, field_id, rect_id):
"""Cập nhật kích thước của bounding box và cập nhật nội dung trường nếu cần."""
# Lấy tọa độ mới của bounding box
coords = self.canvas.coords(rect_id)
x0, y0, x1, y1 = coords
new_width = (x1 - x0) / self.zoom_level
new_height = (y1 - y0) / self.zoom_level
# Cập nhật kích thước của bounding box trong item
for item in self.coords[self.current_page_index]:
if item["rect_id"] == rect_id:
if item.get('type') == 'Text':
item["width"] = new_width
item["height"] = new_height
elif item.get('type') == 'Image':
item["width_points"] = new_width
item["height_points"] = new_height
# Cập nhật vị trí trung tâm
item["x"] = (x0 + x1) / (2 * self.zoom_level)
item["y"] = (y0 + y1) / (2 * self.zoom_level)
# Nếu là hình ảnh, cần cập nhật lại hình ảnh trên canvas
if item.get('type') == 'Image':
# Xóa hình ảnh cũ
self.canvas.delete(item["id"])
# Lấy dữ liệu hình ảnh từ tệp Excel
image_loaded = False
if self.df is not None and not self.df.empty:
row_index = self.excel_row_index if self.excel_row_index is not None else 0
if row_index >= len(self.df):
row_index = len(self.df) - 1
elif row_index < 0:
row_index = 0
row = self.df.iloc[row_index]
field_name = item['field']
if field_name in row:
image_path = str(row[field_name])
if not os.path.isabs(image_path):
image_path = os.path.join(os.path.dirname(self.excel_path), image_path)
if os.path.exists(image_path):
try:
width = new_width * self.zoom_level
height = new_height * self.zoom_level
image = Image.open(image_path)
image = image.resize((int(width), int(height)), Image.LANCZOS)
photo_image = ImageTk.PhotoImage(image)
image_id = self.canvas.create_image(item["x"] * self.zoom_level, item["y"] * self.zoom_level,
image=photo_image, anchor='center')
# Lưu trữ ảnh để tránh bị xóa bởi garbage collector
if not hasattr(self, 'image_refs'):
self.image_refs = []
self.image_refs.append(photo_image)
item["id"] = image_id # Cập nhật ID
image_loaded = True
except Exception as e:
print(f"Lỗi khi tải ảnh '{image_path}': {e}")
# Nếu không tải được hình ảnh, sử dụng placeholder
if not image_loaded:
item["id"] = self.canvas.create_rectangle(x0, y0, x1, y1, outline='blue', dash=(2, 2))
# Đảm bảo hình ảnh được hiển thị phía trên bounding box và anchors
self.canvas.tag_raise(item["id"], item["rect_id"])
for anchor in item.get("anchors", []):
self.canvas.tag_raise(anchor, item["rect_id"])
break
def update_anchor_positions(self, item):
"""Định vị lại các điểm neo dựa trên hình chữ nhật đã thay đổi kích thước."""
rect_id = item["rect_id"]
anchors = item["anchors"]
coords = self.canvas.coords(rect_id)
x0, y0, x1, y1 = coords
# Định nghĩa kích thước của điểm neo
anchor_size = 6
# Vị trí mới cho các điểm neo
positions = [
(x0, y0), # Trên - trái
(x1, y0), # Trên - phải
(x1, y1), # Dưới - phải
(x0, y1), # Dưới - trái
]
for anchor, (x, y) in zip(anchors, positions):
self.canvas.coords(
anchor,
x - anchor_size / 2, y - anchor_size / 2,
x + anchor_size / 2, y + anchor_size / 2
)
def move_text(self, event):
"""Xử lý việc di chuyển các trường văn bản hoặc hình ảnh trên canvas."""
if self.selected_items:
dx = event.x - self.last_move_x
dy = event.y - self.last_move_y
for item_id in self.selected_items.copy():
field = self.get_field_by_text_id(item_id)
if field:
# Di chuyển hình chữ nhật bao quanh
self.canvas.move(field["rect_id"], dx, dy)
# Di chuyển các điểm neo
for anchor in field.get("anchors", []):
self.canvas.move(anchor, dx, dy)
# Di chuyển văn bản hoặc hình ảnh
self.canvas.move(field["id"], dx, dy)
# Cập nhật tọa độ
field["x"] += dx / self.zoom_level
field["y"] += dy / self.zoom_level
self.last_move_x = event.x
self.last_move_y = event.y
self.undo_stack.append(('move', list(self.selected_items), dx / self.zoom_level, dy / self.zoom_level))
self.redo_stack.clear()
self.update_preview()
def update_canvas_font(self, event=None):
"""Cập nhật thuộc tính font của văn bản được chọn."""
if self.selected_items:
for item_id in self.selected_items:
field = self.get_field_by_text_id(item_id)
if field and field.get('type', 'Text') == 'Text':
# Kiểm tra kích thước font
try:
size = int(self.font_size_var.get())
if size <= 0:
raise ValueError
except ValueError:
messagebox.showerror("Lỗi nhập liệu", "Kích thước font phải là số nguyên dương.")
self.font_size_var.set(12)
return
self.canvas.itemconfig(item_id, font=(self.font_var.get(), int(self.font_size_var.get() * self.zoom_level)))
c = self.cmyk_vars['C'].get() / 100
m = self.cmyk_vars['M'].get() / 100
y = self.cmyk_vars['Y'].get() / 100
k = self.cmyk_vars['K'].get() / 100
r, g, b = self.cmyk_to_rgb(c, m, y, k)
color = f'#{int(r):02x}{int(g):02x}{int(b):02x}'
self.canvas.itemconfig(item_id, fill=color)
field["font"] = self.font_var.get()
field["size"] = self.font_size_var.get()
field["color_cmyk"] = {
"C": self.cmyk_vars["C"].get(),
"M": self.cmyk_vars["M"].get(),
"Y": self.cmyk_vars["Y"].get(),
"K": self.cmyk_vars["K"].get(),
}
self.update_preview()
def delete_text(self, event=None):
"""Xóa các trường văn bản hoặc hình ảnh được chọn khỏi canvas."""
if self.selected_items:
for item_id in list(self.selected_items):
field = self.get_field_by_text_id(item_id)
if field:
# Xóa các mục liên quan
items_to_delete = [field["id"], field["rect_id"]] + field.get("anchors", [])
if field.get("label_id"):
items_to_delete.append(field["label_id"])
for del_id in items_to_delete:
self.canvas.delete(del_id)
self.coords[self.current_page_index].remove(field)
self.undo_stack.append(('delete', field))
self.redo_stack.clear() # Xóa ngăn xếp redo
self.selected_items.clear()
self.update_preview()
def undo(self, event=None):
"""Hoàn tác hành động cuối cùng."""
if self.undo_stack:
action = self.undo_stack.pop()
if action[0] == 'add':
# Xóa mục đã thêm
item = action[1]
self.canvas.delete(item["id"])
if item.get("label_id"):
self.canvas.delete(item["label_id"])
for anchor in item.get("anchors", []):
self.canvas.delete(anchor)
self.canvas.delete(item.get("rect_id", ""))
self.coords[self.current_page_index].remove(item)
self.redo_stack.append(('add', item))
elif action[0] == 'move':
# Di chuyển trở lại vị trí trước đó
item_ids, dx, dy = action[1:]
for item_id in item_ids:
self.canvas.move(item_id, -dx * self.zoom_level, -dy * self.zoom_level)
field = self.get_field_by_text_id(item_id)
if field:
self.canvas.move(field.get("label_id", ""), -dx * self.zoom_level, -dy * self.zoom_level)
for anchor in field.get("anchors", []):
self.canvas.move(anchor, -dx * self.zoom_level, -dy * self.zoom_level)
field["x"] -= dx
field["y"] -= dy
self.redo_stack.append(('move', item_ids, dx, dy))
elif action[0] == 'delete':
# Thêm lại mục đã xóa
item = action[1]
if item.get('type', 'Text') == 'Text':
self.add_text(item["x"] * self.zoom_level, item["y"] * self.zoom_level)
elif item.get('type') == 'Image':
self.add_text(item["x"] * self.zoom_level, item["y"] * self.zoom_level)
self.coords[self.current_page_index].append(item)
self.redo_stack.append(('delete', item))
self.update_preview()
def redo(self, event=None):
"""Làm lại hành động đã hoàn tác."""
if self.redo_stack:
action = self.redo_stack.pop()
if action[0] == 'add':
# Thêm lại mục
item = action[1]
self.add_text(item["x"] * self.zoom_level, item["y"] * self.zoom_level)
self.coords[self.current_page_index].append(item)
self.undo_stack.append(('add', item))
elif action[0] == 'move':
# Di chuyển đến vị trí mới
item_ids, dx, dy = action[1:]
for item_id in item_ids:
self.canvas.move(item_id, dx * self.zoom_level, dy * self.zoom_level)
field = self.get_field_by_text_id(item_id)
if field:
self.canvas.move(field.get("label_id", ""), dx * self.zoom_level, dy * self.zoom_level)
for anchor in field.get("anchors", []):
self.canvas.move(anchor, dx * self.zoom_level, dy * self.zoom_level)
field["x"] += dx
field["y"] += dy
self.undo_stack.append(('move', item_ids, dx, dy))
elif action[0] == 'delete':
# Xóa mục
item = action[1]
self.canvas.delete(item["id"])
if item.get("label_id"):
self.canvas.delete(item.get("label_id"))
for anchor in item.get("anchors", []):
self.canvas.delete(anchor)
self.canvas.delete(item.get("rect_id", ""))
self.coords[self.current_page_index].remove(item)
self.undo_stack.append(('delete', item))
self.update_preview()
def load_excel(self):
"""Load Excel file and display data."""
self.excel_path = filedialog.askopenfilename(filetypes=[("Excel files", ".xlsx"), ("Excel files", ".xls")])
if self.excel_path:
try:
self.df = pd.read_excel(self.excel_path)
# Generate numbering data matching the length of Excel data
numbering_df = self.generate_numbering_data(num_rows=len(self.df))
self.df['Numbering'] = numbering_df['Numbering'].values
# Add 'Numbering' to the list of fields if not already present
if 'Numbering' not in self.df.columns:
self.df['Numbering'] = numbering_df['Numbering'].values
self.fields = self.df.columns.tolist()
self.selected_field.set(self.fields[0]) # Default selected field
self.create_field_radiobuttons()
self.reset_preview()
self.excel_row_index = 0 # Reset index to 0
except Exception as e:
messagebox.showerror("Error", f"Cannot read Excel file: {e}")
self.excel_path = None
self.df = None
else:
# If no Excel file is selected, use Numbering data only
self.df = self.generate_numbering_data()
if self.df.empty:
messagebox.showerror("Error", "Numbering data is empty. Please check the parameters.")
return
self.fields = ['Numbering']
self.selected_field.set('Numbering') # Set default field
self.create_field_radiobuttons()
self.reset_preview()
self.excel_row_index = 0
def create_field_radiobuttons(self):
"""Tạo các nút radio cho các trường dữ liệu Excel."""
# Xóa khung nút hiện có và tạo mới
for widget in self.excel_button_frame.winfo_children():
widget.destroy()
# Tạo canvas để chứa các nút với thanh cuộn
canvas = tk.Canvas(self.excel_button_frame, bg='#2c2f33', highlightthickness=0)
scrollbar = tk.Scrollbar(self.excel_button_frame, orient=tk.HORIZONTAL, command=canvas.xview)
canvas.configure(xscrollcommand=scrollbar.set)
inner_frame = tk.Frame(canvas, bg='#2c2f33')
# Thêm inner_frame vào canvas
canvas.create_window((0, 0), window=inner_frame, anchor='nw')
canvas.pack(side=tk.TOP, fill=tk.X, expand=True)
scrollbar.pack(side=tk.BOTTOM, fill=tk.X)
# Cập nhật scrollregion sau khi thêm widget
def configure_canvas(event):
canvas.configure(scrollregion=canvas.bbox("all"))
inner_frame.bind("<Configure>", configure_canvas)
# Đặt tất cả các nút trong inner_frame
for field in self.fields:
rb = tk.Radiobutton(inner_frame, text=field, variable=self.selected_field, value=field,
bg='#2c2f33', fg='white', selectcolor='#7289da')
rb.pack(side=tk.LEFT, padx=5, pady=5)
canvas.config(scrollregion=canvas.bbox("all"))
def cmyk_to_rgb(self, c, m, y, k):
"""Chuyển đổi giá trị CMYK sang RGB."""
r = min(max(0, 255 * (1 - c) * (1 - k)), 255)
g = min(max(0, 255 * (1 - m) * (1 - k)), 255)
b = min(max(0, 255 * (1 - y) * (1 - k)), 255)
return int(r), int(g), int(b)
def mm_to_points(self, mm):
"""Chuyển đổi millimeters sang points."""
return mm * 72 / 25.4
def points_to_mm(self, points):
"""Chuyển đổi points sang millimeters."""
return points * 25.4 / 72
def create_text_pdf(self, output_pdf_path, row, selected_font_path):
"""Tạo tệp PDF tùy chỉnh với các trường văn bản và hình ảnh."""
c = reportlab_canvas.Canvas(output_pdf_path, pagesize=(self.original_width, self.original_height))
total_pages = len(self.pdf_doc)
for page_index in range(total_pages):
c.setPageSize((self.original_width, self.original_height))
if page_index in self.coords:
fields = self.coords[page_index]
for item in fields:
field_name = item["field"]
if field_name not in row:
print(f"Trường '{field_name}' không có trong dữ liệu. Bỏ qua...")
continue
x = item["x"]
y = item["y"]
if item.get('type', 'Text') == 'Text':
font_name = item["font"]
size = item["size"]
color_cmyk = item["color_cmyk"]
bounding_width = item["width"]
bounding_height = item["height"]
# Lấy đường dẫn font
font_path = self.font_file_dict.get(font_name, selected_font_path)
if not self.check_font_availability(font_path):
font_path = selected_font_path
# Đăng ký và đặt font
if font_name not in pdfmetrics.getRegisteredFontNames():
pdfmetrics.registerFont(TTFont(font_name, font_path))
value = str(row[field_name])
# Cài đặt màu
c.setFillColorCMYK(
color_cmyk["C"] / 100,
color_cmyk["M"] / 100,
color_cmyk["Y"] / 100,
color_cmyk["K"] / 100
)
# Tính kích thước nội dung
text_width = pdfmetrics.stringWidth(value, font_name, size)
text_height = size # Xấp xỉ chiều cao bằng kích thước font
# Tính toán scale
scale_x = scale_y = 1.0
if text_width > bounding_width:
scale_x = bounding_width / text_width
if text_height > bounding_height:
scale_y = bounding_height / text_height
scale_option_display = self.scale_option.get()
scale_option = self.scale_option_mapping.get(scale_option_display, "uniform")
if scale_option == "uniform":
scale = min(scale_x, scale_y)
new_size = size * scale
c.setFont(font_name, new_size)
text_width_scaled = pdfmetrics.stringWidth(value, font_name, new_size)
adjusted_y = self.original_height - y - new_size / 2
adjusted_x = x - text_width_scaled / 2
c.drawString(adjusted_x, adjusted_y, value)
elif scale_option == "horizontal":
scale = scale_x
new_size = size # Giữ nguyên kích thước font
c.setFont(font_name, new_size)
c.saveState()
c.translate(x, self.original_height - y)
c.scale(scale, 1)
adjusted_x = -text_width / 2
adjusted_y = -new_size / 2
c.drawString(adjusted_x, adjusted_y, value)
c.restoreState()
elif scale_option == "vertical":
scale = scale_y
new_size = size # Giữ nguyên kích thước font
c.setFont(font_name, new_size)
c.saveState()
c.translate(x, self.original_height - y)
c.scale(1, scale)
adjusted_x = -text_width / 2
adjusted_y = -new_size / 2
c.drawString(adjusted_x, adjusted_y, value)
c.restoreState()
elif scale_option == "kerning":
# Điều chỉnh khoảng cách giữa các ký tự
total_characters = len(value)
if total_characters > 1:
total_space_needed = text_width - bounding_width
if total_space_needed > 0:
extra_space_per_char = total_space_needed / (total_characters - 1)
c.setFont(font_name, size)
adjusted_x_positions = []
current_x = x - text_width / 2
for char in value:
adjusted_x_positions.append(current_x)
char_width = pdfmetrics.stringWidth(char, font_name, size)
current_x += char_width - extra_space_per_char
# Vẽ từng ký tự tại vị trí mới
adjusted_y = self.original_height - y - size / 2
for i, char in enumerate(value):
c.drawString(adjusted_x_positions[i], adjusted_y, char)
elif item.get('type') == 'Image':
image_path = str(row[field_name])
if not os.path.isabs(image_path):
image_path = os.path.join(os.path.dirname(self.excel_path), image_path)
if not os.path.exists(image_path):
print(f"Hình ảnh '{image_path}' không tồn tại. Bỏ qua...")
continue
# Sử dụng chiều rộng và chiều cao đã lưu
width_in_points = item["width_points"]
height_in_points = item["height_points"]
adjusted_y = self.original_height - y - height_in_points / 2
adjusted_x = x - width_in_points / 2
try:
c.drawImage(image_path, adjusted_x, adjusted_y,
width=width_in_points,
height=height_in_points)
except Exception as e:
print(f"Lỗi khi chèn hình ảnh: {e}")
c.showPage()
c.save()
def export_pdfs(self):
"""Export PDF files with data from Excel or numbering, and merge them."""
if not self.file_path:
messagebox.showerror("Lỗi", "Hãy chọn tệp trước khi xuất.")
return
if not self.coords:
messagebox.showerror("Lỗi", "Chưa đặt trường nào trên tệp.")
return
# If self.df is None or empty, generate numbering data
if self.df is None or self.df.empty:
self.df = self.generate_numbering_data()
if self.df.empty:
messagebox.showerror("Lỗi", "Dữ liệu đánh số trống. Vui lòng kiểm tra các tham số.")
return
self.fields = ['Numbering']
self.selected_field.set('Numbering')
self.create_field_radiobuttons()
self.excel_row_index = 0 # Reset index to 0
# Ask user to select the output folder
output_folder = filedialog.askdirectory()
if not output_folder:
return
# Create a 'data' subfolder inside the selected output folder
data_folder = os.path.join(output_folder, 'data')
os.makedirs(data_folder, exist_ok=True)
selected_font_name = self.font_var.get()
selected_font_path = self.font_file_dict.get(selected_font_name, None)
if not selected_font_path or not self.check_font_availability(selected_font_path):
messagebox.showerror("Lỗi", f"Không thể mở tệp font {selected_font_name}, sử dụng font mặc định.")
if sys.platform == "win32":
selected_font_path = "C:/Windows/Fonts/Arial.ttf"
else:
selected_font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
# List to store paths of final PDFs
individual_pdfs = []
temp_dir = tempfile.gettempdir()
# Điều chỉnh phần này để tạo hai trang PDF cho mỗi hàng dữ liệu
for index, row in self.df.iterrows():
# Open the original PDF template
doc = fitz.open(self.temp_pdf_path)
# Create a unique name for the final output file
fields_in_name = '_'.join([str(row[field]) for field in self.fields[:3]]) # Customize as needed
if not fields_in_name.strip():
fields_in_name = f'output_{index}'
# File path for the final output PDF in the 'data' folder
final_output_path = os.path.join(data_folder, f"{fields_in_name}_final.pdf")
# Generate the custom data PDF with two pages in the temp directory
temp_output_pdf_path = os.path.join(temp_dir, f"temp_custom_{uuid.uuid4()}.pdf")
# Create custom PDF with two pages
self.create_text_pdf_with_two_pages(temp_output_pdf_path, row, selected_font_path)
# Open the custom data PDF
new_doc = fitz.open(temp_output_pdf_path)
# Ensure the custom PDF has the same number of pages as the original
if new_doc.page_count != len(doc):
messagebox.showerror("Lỗi", "Số trang trong tệp gốc và tệp mới không khớp.")
new_doc.close()
doc.close()
continue
# Overlay the custom data onto the original PDF
for page_num in range(len(doc)):
page = doc.load_page(page_num)
rect = fitz.Rect(0, 0, self.original_width, self.original_height)
page.show_pdf_page(rect, new_doc, page_num)
new_doc.close()
# Save the final PDF into the 'data' folder
doc.save(final_output_path)
individual_pdfs.append(final_output_path)
doc.close()
# Remove the temporary custom PDF
try:
os.remove(temp_output_pdf_path)
except Exception as e:
print(f"Lỗi khi xóa tệp tạm: {e}")
# Merge the individual final PDFs into one, saved in the selected output folder
if individual_pdfs:
merged_pdf_path = os.path.join(output_folder, "merged_final_output.pdf")
self.merge_pdfs(individual_pdfs, merged_pdf_path)
# Apply imposition automatically
# Get parameters from the UI
new_page_width = self.new_page_width_var.get()
new_page_height = self.new_page_height_var.get()
margin_top = self.margin_top_var.get()
margin_bottom = self.margin_bottom_var.get()
margin_left = self.margin_left_var.get()
margin_right = self.margin_right_var.get()
cropmark_offset = self.cropmark_offset_var.get()
# Convert units from mm to points
new_page_width_pt = self.mm_to_points(new_page_width)
new_page_height_pt = self.mm_to_points(new_page_height)
margin_top_pt = self.mm_to_points(margin_top)
margin_bottom_pt = self.mm_to_points(margin_bottom)
margin_left_pt = self.mm_to_points(margin_left)
margin_right_pt = self.mm_to_points(margin_right)
cropmark_offset_pt = self.mm_to_points(cropmark_offset)
# Apply imposition, save the imposed PDF in the selected output folder
imposed_pdf_path = os.path.join(output_folder, "imposed_output.pdf")
self.apply_imposition(merged_pdf_path, imposed_pdf_path)
# Inform the user that all three results have been successfully exported
messagebox.showinfo("Thành công", f"File PDF đã được xuất thành công.\n\n- Các file PDF cá nhân được lưu trong thư mục 'data'.\n- File PDF đã merge: {merged_pdf_path}\n- File PDF đã áp dụng imposition: {imposed_pdf_path}")
else:
messagebox.showerror("Lỗi", "Không có file PDF nào được tạo.")
def create_text_pdf_with_two_pages(self, output_pdf_path, row, selected_font_path):
"""Create a two-page PDF with the same data for front and back."""
c = reportlab_canvas.Canvas(output_pdf_path, pagesize=(self.original_width, self.original_height))
for page_index in range(2): # Tạo hai trang
c.setPageSize((self.original_width, self.original_height))
if page_index in self.coords:
fields = self.coords[page_index]
else:
fields = self.coords.get(0, []) # Nếu không có coords cho trang 1, sử dụng trang 0
for item in fields:
field_name = item["field"]
if field_name not in row:
print(f"Trường '{field_name}' không có trong dữ liệu. Bỏ qua...")
continue
x = item["x"]
y = item["y"]
if item.get('type', 'Text') == 'Text':
font_name = item["font"]
size = item["size"]
color_cmyk = item["color_cmyk"]
bounding_width = item["width"]
bounding_height = item["height"]
# Lấy đường dẫn font
font_path = self.font_file_dict.get(font_name, selected_font_path)
if not self.check_font_availability(font_path):
font_path = selected_font_path
# Đăng ký và đặt font
if font_name not in pdfmetrics.getRegisteredFontNames():
pdfmetrics.registerFont(TTFont(font_name, font_path))
value = str(row[field_name])
# Cài đặt màu
c.setFillColorCMYK(
color_cmyk["C"] / 100,
color_cmyk["M"] / 100,
color_cmyk["Y"] / 100,
color_cmyk["K"] / 100
)
# Tính kích thước nội dung
text_width = pdfmetrics.stringWidth(value, font_name, size)
text_height = size # Xấp xỉ chiều cao bằng kích thước font
# Tính toán scale
scale_x = scale_y = 1.0
if text_width > bounding_width:
scale_x = bounding_width / text_width
if text_height > bounding_height:
scale_y = bounding_height / text_height
scale_option_display = self.scale_option.get()
scale_option = self.scale_option_mapping.get(scale_option_display, "uniform")
if scale_option == "uniform":
scale = min(scale_x, scale_y)
new_size = size * scale
c.setFont(font_name, new_size)
text_width_scaled = pdfmetrics.stringWidth(value, font_name, new_size)
adjusted_y = self.original_height - y - new_size / 2
adjusted_x = x - text_width_scaled / 2
c.drawString(adjusted_x, adjusted_y, value)
elif scale_option == "horizontal":
scale = scale_x
new_size = size # Giữ nguyên kích thước font
c.setFont(font_name, new_size)
c.saveState()
c.translate(x, self.original_height - y)
c.scale(scale, 1)
adjusted_x = -text_width / 2
adjusted_y = -new_size / 2
c.drawString(adjusted_x, adjusted_y, value)
c.restoreState()
elif scale_option == "vertical":
scale = scale_y
new_size = size # Giữ nguyên kích thước font
c.setFont(font_name, new_size)
c.saveState()
c.translate(x, self.original_height - y)
c.scale(1, scale)
adjusted_x = -text_width / 2
adjusted_y = -new_size / 2
c.drawString(adjusted_x, adjusted_y, value)
c.restoreState()
elif scale_option == "kerning":
# Điều chỉnh khoảng cách giữa các ký tự
total_characters = len(value)
if total_characters > 1:
total_space_needed = text_width - bounding_width
if total_space_needed > 0:
extra_space_per_char = total_space_needed / (total_characters - 1)
c.setFont(font_name, size)
adjusted_x_positions = []
current_x = x - text_width / 2
for char in value:
adjusted_x_positions.append(current_x)
char_width = pdfmetrics.stringWidth(char, font_name, size)
current_x += char_width - extra_space_per_char
# Vẽ từng ký tự tại vị trí mới
adjusted_y = self.original_height - y - size / 2
for i, char in enumerate(value):
c.drawString(adjusted_x_positions[i], adjusted_y, char)
elif item.get('type') == 'Image':
image_path = str(row[field_name])
if not os.path.isabs(image_path):
image_path = os.path.join(os.path.dirname(self.excel_path), image_path)
if not os.path.exists(image_path):
print(f"Hình ảnh '{image_path}' không tồn tại. Bỏ qua...")
continue
# Sử dụng chiều rộng và chiều cao đã lưu
width_in_points = item["width_points"]
height_in_points = item["height_points"]
adjusted_y = self.original_height - y - height_in_points / 2
adjusted_x = x - width_in_points / 2
try:
c.drawImage(image_path, adjusted_x, adjusted_y,
width=width_in_points,
height=height_in_points)
except Exception as e:
print(f"Lỗi khi chèn hình ảnh: {e}")
c.showPage()
c.save()
def merge_pdfs(self, pdf_paths, output_path):
"""Merge nhiều tệp PDF thành một."""
merged_doc = fitz.open()
for pdf_path in pdf_paths:
individual_doc = fitz.open(pdf_path)
merged_doc.insert_pdf(individual_doc)
individual_doc.close()
merged_doc.save(output_path)
merged_doc.close()
def align_left(self):
"""Căn trái các mục đã chọn."""
if not self.selected_items:
messagebox.showinfo("Thông báo", "Chưa chọn mục nào để căn chỉnh.")
return
min_x = None
for item_id in self.selected_items:
field = self.get_field_by_text_id(item_id)
if field:
x = field['x']
if min_x is None or x < min_x:
min_x = x
for item_id in self.selected_items:
field = self.get_field_by_text_id(item_id)
if field:
dx = (min_x - field['x']) * self.zoom_level
self.canvas.move(item_id, dx, 0)
field['x'] = min_x
if field.get('label_id'):
self.canvas.move(field['label_id'], dx, 0)
self.update_preview()
def align_right(self):
"""Căn phải các mục đã chọn."""
if not self.selected_items:
messagebox.showinfo("Thông báo", "Chưa chọn mục nào để căn chỉnh.")
return
max_x = None
for item_id in self.selected_items:
field = self.get_field_by_text_id(item_id)
if field:
x = field['x']
if max_x is None or x > max_x:
max_x = x
for item_id in self.selected_items:
field = self.get_field_by_text_id(item_id)
if field:
dx = (max_x - field['x']) * self.zoom_level
self.canvas.move(item_id, dx, 0)
field['x'] = max_x
if field.get('label_id'):
self.canvas.move(field['label_id'], dx, 0)
self.update_preview()
def align_top(self):
"""Căn trên các mục đã chọn."""
if not self.selected_items:
messagebox.showinfo("Thông báo", "Chưa chọn mục nào để căn chỉnh.")
return
min_y = None
for item_id in self.selected_items:
field = self.get_field_by_text_id(item_id)
if field:
y = field['y']
if min_y is None or y < min_y:
min_y = y
for item_id in self.selected_items:
field = self.get_field_by_text_id(item_id)
if field:
dy = (min_y - field['y']) * self.zoom_level
self.canvas.move(item_id, 0, dy)
field['y'] = min_y
if field.get('label_id'):
self.canvas.move(field['label_id'], 0, dy)
self.update_preview()
def align_bottom(self):
"""Căn dưới các mục đã chọn."""
if not self.selected_items:
messagebox.showinfo("Thông báo", "Chưa chọn mục nào để căn chỉnh.")
return
max_y = None
for item_id in self.selected_items:
field = self.get_field_by_text_id(item_id)
if field:
y = field['y']
if max_y is None or y > max_y:
max_y = y
for item_id in self.selected_items:
field = self.get_field_by_text_id(item_id)
if field:
dy = (max_y - field['y']) * self.zoom_level
self.canvas.move(item_id, 0, dy)
field['y'] = max_y
if field.get('label_id'):
self.canvas.move(field['label_id'], 0, dy)
self.update_preview()
def align_center(self):
"""Căn giữa các mục đã chọn."""
if not self.selected_items:
messagebox.showinfo("Thông báo", "Chưa chọn mục nào để căn chỉnh.")
return
total_x = 0
count = 0
for item_id in self.selected_items:
field = self.get_field_by_text_id(item_id)
if field:
total_x += field['x']
count += 1
if count == 0:
return
avg_x = total_x / count
for item_id in self.selected_items:
field = self.get_field_by_text_id(item_id)
if field:
dx = (avg_x - field['x']) * self.zoom_level
self.canvas.move(item_id, dx, 0)
field['x'] = avg_x
if field.get('label_id'):
self.canvas.move(field['label_id'], dx, 0)
self.update_preview()
def zoom_in(self):
"""Phóng to canvas."""
self.zoom_level *= 1.1
self.display_pdf_page()
# self.display_preview_page()
def zoom_out(self):
"""Thu nhỏ canvas."""
self.zoom_level /= 1.1
self.display_pdf_page()
# self.display_preview_page()
def zoom_with_mouse_wheel(self, event):
"""Phóng to hoặc thu nhỏ bằng con lăn chuột."""
if event.delta > 0 or event.num == 4:
self.zoom_in()
elif event.delta < 0 or event.num == 5:
self.zoom_out()
def preview_pdf(self):
"""Tạo và hiển thị PDF xem trước trong một cửa sổ mới."""
if not self.file_path or not self.excel_path:
messagebox.showerror("Lỗi", "Hãy chọn tệp và Excel trước khi xem trước.")
return
if not self.coords:
messagebox.showerror("Lỗi", "Chưa đặt trường nào trên tệp.")
return
# Tạo PDF xem trước trong thư mục tạm
temp_dir = tempfile.gettempdir()
self.preview_pdf_path = os.path.join(temp_dir, "preview.pdf")
selected_font_name = self.font_var.get()
selected_font_path = self.font_file_dict.get(selected_font_name, None)
if not selected_font_path or not self.check_font_availability(selected_font_path):
if sys.platform == "win32":
selected_font_path = "C:/Windows/Fonts/Arial.ttf"
else:
selected_font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
# Tạo một file PDF duy nhất chứa tất cả các trang với dữ liệu được điền
doc = fitz.open()
# Danh sách lưu các tệp PDF cá nhân
individual_pdfs = []
for index, row in self.df.iterrows():
temp_pdf_path = os.path.join(temp_dir, f"temp_data_{uuid.uuid4()}.pdf")
# Tạo PDF tùy chỉnh
self.create_text_pdf(temp_pdf_path, row, selected_font_path)
# Mở PDF tùy chỉnh và thêm vào doc
temp_doc = fitz.open(temp_pdf_path)
doc.insert_pdf(temp_doc)
temp_doc.close()
# Thêm vào danh sách để xóa sau này
individual_pdfs.append(temp_pdf_path)
# Lưu tài liệu PDF với dữ liệu
output_pdf_path = os.path.join(temp_dir, "output_with_data_preview.pdf")
doc.save(output_pdf_path)
doc.close()
# Áp dụng Imposition và lưu vào tệp mới
imposed_preview_pdf_path = os.path.join(temp_dir, "imposed_preview.pdf")
self.apply_imposition(output_pdf_path, imposed_preview_pdf_path)
# Xóa các tệp PDF tạm thời
try:
for pdf_path in individual_pdfs:
os.remove(pdf_path)
os.remove(output_pdf_path)
except Exception as e:
print(f"Lỗi khi xóa tệp tạm: {e}")
# Mở tệp xem trước đã áp dụng imposition
self.preview_pdf_path = imposed_preview_pdf_path
if self.preview_doc is not None:
self.preview_doc.close()
self.preview_doc = fitz.open(self.preview_pdf_path)
self.preview_page_index = 0
# Tạo cửa sổ xem trước mới nếu chưa có
if hasattr(self, 'preview_window') and self.preview_window.winfo_exists():
# Nếu cửa sổ đã tồn tại, chỉ cần cập nhật canvas
self.display_preview_page()
else:
# Tạo cửa sổ xem trước mới
self.preview_window = tk.Toplevel(self.root)
self.preview_window.title("Xem trước")
self.preview_window.geometry("800x600")
# Thêm sự kiện khi đóng cửa sổ
self.preview_window.protocol("WM_DELETE_WINDOW", self.close_preview_window)
# Tạo canvas trong cửa sổ xem trước
self.preview_canvas = tk.Canvas(self.preview_window, bg="gray")
self.preview_canvas.pack(fill=tk.BOTH, expand=True)
# Thêm thanh cuộn cho canvas
hbar = tk.Scrollbar(self.preview_window, orient=tk.HORIZONTAL)
hbar.pack(side=tk.BOTTOM, fill=tk.X)
hbar.config(command=self.preview_canvas.xview)
vbar = tk.Scrollbar(self.preview_window, orient=tk.VERTICAL)
vbar.pack(side=tk.RIGHT, fill=tk.Y)
vbar.config(command=self.preview_canvas.yview)
self.preview_canvas.config(xscrollcommand=hbar.set, yscrollcommand=vbar.set)
# Hiển thị trang xem trước
self.display_preview_page()
self.is_previewing = True
def display_preview_page(self):
"""Hiển thị trang xem trước trong cửa sổ xem trước."""
if self.preview_doc:
try:
page = self.preview_doc.load_page(self.preview_page_index)
zoom_matrix = fitz.Matrix(self.zoom_level, self.zoom_level)
pix = page.get_pixmap(matrix=zoom_matrix)
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
self.preview_image = ImageTk.PhotoImage(img)
self.preview_canvas.delete("all")
self.preview_canvas.create_image(0, 0, anchor="nw", image=self.preview_image)
# Cập nhật kích thước canvas
self.preview_canvas.config(width=pix.width, height=pix.height)
self.preview_canvas.config(scrollregion=self.preview_canvas.bbox(tk.ALL))
except Exception as e:
messagebox.showerror("Lỗi", f"Không thể hiển thị trang: {e}")
def close_preview_window(self):
"""Xử lý khi đóng cửa sổ xem trước."""
if self.preview_doc is not None:
self.preview_doc.close()
self.preview_doc = None
if hasattr(self, 'preview_window') and self.preview_window.winfo_exists():
self.preview_window.destroy()
self.is_previewing = False
def update_preview(self):
"""Cập nhật xem trước khi có thay đổi."""
if self.is_previewing and hasattr(self, 'preview_window') and self.preview_window.winfo_exists():
self.preview_pdf()
def reset_preview(self):
"""Đặt lại xem trước khi tải tệp hoặc Excel mới."""
if hasattr(self, 'preview_window') and self.preview_window.winfo_exists():
self.preview_window.destroy()
self.preview_doc = None
self.preview_pdf_path = None
self.preview_page_index = 0
self.is_previewing = False
self.excel_row_index = 0 # Đặt lại chỉ số hàng dữ liệu..
def on_closing(self):
"""Xử lý khi đóng ứng dụng."""
if hasattr(self, 'temp_pdf_path') and os.path.exists(self.temp_pdf_path):
try:
os.remove(self.temp_pdf_path)
except Exception as e:
print(f"Lỗi khi xóa tệp tạm: {e}")
if self.preview_doc is not None:
self.preview_doc.close()
if hasattr(self, 'preview_window') and self.preview_window.winfo_exists():
self.preview_window.destroy()
self.root.destroy()
def on_imposition_option_changed(self, event):
"""Xử lý khi người dùng thay đổi tùy chọn imposition."""
# Nếu cần cập nhật hoặc thực hiện hành động gì khi tùy chọn thay đổi, bạn thêm mã xử lý tại đây
pass
def apply_imposition(self, input_pdf_path, output_pdf_path):
"""Áp dụng Imposition để sắp xếp nhiều dữ liệu trên một trang."""
# Lấy loại imposition người dùng đã chọn
imposition_type = self.imposition_option.get()
if imposition_type == "Bình zigzag":
self.apply_zigzag_imposition(input_pdf_path, output_pdf_path)
elif imposition_type == "Bình tự trở":
self.apply_self_turn_imposition(input_pdf_path, output_pdf_path)
elif imposition_type == "Bình AB":
self.apply_ab_imposition(input_pdf_path, output_pdf_path)
else:
messagebox.showerror("Lỗi", "Loại imposition không được hỗ trợ.")
def apply_zigzag_imposition(self, input_pdf_path, output_pdf_path):
"""Applies Imposition to arrange multiple data on a page in Zigzag style."""
# Get parameters from the UI
new_page_width = self.new_page_width_var.get()
new_page_height = self.new_page_height_var.get()
margin_top = self.margin_top_var.get()
margin_bottom = self.margin_bottom_var.get()
margin_left = self.margin_left_var.get()
margin_right = self.margin_right_var.get()
cropmark_offset = self.cropmark_offset_var.get()
cropmark_length = self.cropmark_length_var.get()
# Convert units from mm to points
new_page_width_pt = self.mm_to_points(new_page_width)
new_page_height_pt = self.mm_to_points(new_page_height)
margin_top_pt = self.mm_to_points(margin_top)
margin_bottom_pt = self.mm_to_points(margin_bottom)
margin_left_pt = self.mm_to_points(margin_left)
margin_right_pt = self.mm_to_points(margin_right)
cropmark_offset_pt = self.mm_to_points(cropmark_offset)
crop_length_pt = self.mm_to_points(cropmark_length)
# Độ dày của crop marks
crop_thickness = 0.5 # Bạn có thể thay đổi giá trị này
# Calculate available area
available_width = new_page_width_pt - margin_left_pt - margin_right_pt
available_height = new_page_height_pt - margin_top_pt - margin_bottom_pt
# Read the input PDF
doc = fitz.open(input_pdf_path)
new_doc = fitz.open()
total_pages = len(doc)
# Get the size of the input page
input_page = doc.load_page(0)
bbox = input_page.rect
entry_width_orig = bbox.width
entry_height_orig = bbox.height
# Options for rotation and non-rotation
options = [
{'rotate': False, 'entry_width': entry_width_orig, 'entry_height': entry_height_orig},
{'rotate': True, 'entry_width': entry_height_orig, 'entry_height': entry_width_orig},
]
# Determine the best arrangement
best_arrangement = None
max_total_entries = 0
for option in options:
entries_per_row = int(available_width // option['entry_width'])
entries_per_column = int(available_height // option['entry_height'])
total_entries = entries_per_row * entries_per_column
if total_entries > max_total_entries:
max_total_entries = total_entries
best_arrangement = option
best_arrangement['entries_per_row'] = entries_per_row
best_arrangement['entries_per_column'] = entries_per_column
if not best_arrangement or best_arrangement['entries_per_row'] == 0 or best_arrangement['entries_per_column'] == 0:
messagebox.showerror("Error", "Page size or margins are too small to fit any content.")
return
# Tính toán lại x_offset và y_offset để căn giữa nội dung trên trang
total_grid_width = best_arrangement['entries_per_row'] * best_arrangement['entry_width']
total_grid_height = best_arrangement['entries_per_column'] * best_arrangement['entry_height']
x_offset = (new_page_width_pt - total_grid_width) / 2
y_offset = (new_page_height_pt - total_grid_height) / 2
page_index = 0
while page_index < total_pages:
new_page = new_doc.new_page(width=new_page_width_pt, height=new_page_height_pt)
arranged_rects = [] # Danh sách lưu vị trí của các đối tượng để xác định cạnh ngoài cùng
for row in range(best_arrangement['entries_per_column']):
y = y_offset + row * best_arrangement['entry_height']
for col in range(best_arrangement['entries_per_row']):
x = x_offset + col * best_arrangement['entry_width']
if page_index >= total_pages:
break
rect = fitz.Rect(
x,
y,
x + best_arrangement['entry_width'],
y + best_arrangement['entry_height']
)
if best_arrangement['rotate']:
new_page.show_pdf_page(rect, doc, pno=page_index, rotate=90)
else:
new_page.show_pdf_page(rect, doc, pno=page_index)
arranged_rects.append((rect, row, col)) # Lưu thông tin vị trí
page_index += 1
if page_index >= total_pages:
break
# Sau khi đặt tất cả các đối tượng trên trang, thêm crop marks vào các cạnh ngoài cùng
for rect, row, col in arranged_rects:
edges = []
if row == 0:
edges.append('top')
if row == best_arrangement['entries_per_column'] - 1:
edges.append('bottom')
if col == 0:
edges.append('left')
if col == best_arrangement['entries_per_row'] - 1:
edges.append('right')
if edges:
self.add_crop_marks(new_page, rect, edges, cropmark_offset_pt, crop_length_pt, crop_thickness)
# Save the imposed PDF
new_doc.save(output_pdf_path)
new_doc.close()
doc.close()
def apply_self_turn_imposition(self, input_pdf_path, output_pdf_path):
"""Applies Imposition to arrange data on a page in Self-Turn style."""
# Get parameters from the UI
new_page_width = self.new_page_width_var.get()
new_page_height = self.new_page_height_var.get()
margin_top = self.margin_top_var.get()
margin_bottom = self.margin_bottom_var.get()
margin_left = self.margin_left_var.get()
margin_right = self.margin_right_var.get()
cropmark_offset = self.cropmark_offset_var.get()
cropmark_length = self.cropmark_length_var.get()
# Convert units from mm to points
new_page_width_pt = self.mm_to_points(new_page_width)
new_page_height_pt = self.mm_to_points(new_page_height)
margin_top_pt = self.mm_to_points(margin_top)
margin_bottom_pt = self.mm_to_points(margin_bottom)
margin_left_pt = self.mm_to_points(margin_left)
margin_right_pt = self.mm_to_points(margin_right)
cropmark_offset_pt = self.mm_to_points(cropmark_offset)
crop_length_pt = self.mm_to_points(cropmark_length)
# Độ dày của crop marks
crop_thickness = 0.5
# Tính diện tích khả dụng trên trang
available_width = new_page_width_pt - margin_left_pt - margin_right_pt
available_height = new_page_height_pt - margin_top_pt - margin_bottom_pt
# Đọc tệp PDF đầu vào
doc = fitz.open(input_pdf_path)
new_doc = fitz.open()
total_pages = len(doc)
# Lấy kích thước của dữ liệu
input_page = doc.load_page(0)
bbox = input_page.rect
entry_width_orig = bbox.width
entry_height_orig = bbox.height
# Các tùy chọn xoay và không xoay
options = [
{'rotate': False, 'entry_width': entry_width_orig, 'entry_height': entry_height_orig},
{'rotate': True, 'entry_width': entry_height_orig, 'entry_height': entry_width_orig},
]
# Tìm cách sắp xếp tốt nhất (chia theo chiều dọc)
best_arrangement = None
max_total_entries = 0
for option in options:
# Chia trang theo chiều dọc (trên và dưới)
entries_per_row = int(available_width // option['entry_width'])
entries_per_column = int((available_height / 2) // option['entry_height'])
total_entries = entries_per_row * entries_per_column
if total_entries > max_total_entries and entries_per_row > 0 and entries_per_column > 0:
max_total_entries = total_entries
best_arrangement = {
'rotate': option['rotate'],
'entries_per_row': entries_per_row,
'entries_per_column': entries_per_column,
'entry_width': option['entry_width'],
'entry_height': option['entry_height'],
}
if not best_arrangement:
messagebox.showerror("Error", "Page size or margins are too small to fit any content.")
return
rotate = best_arrangement['rotate']
entries_per_row = best_arrangement['entries_per_row']
entries_per_column = best_arrangement['entries_per_column']
entry_width = best_arrangement['entry_width']
entry_height = best_arrangement['entry_height']
# Tính toán vị trí x_offset và y_offset để căn giữa nội dung trên trang
total_grid_width = entries_per_row * entry_width
total_grid_height = entries_per_column * entry_height * 2 # Nhân 2 cho hai nửa trên dưới
x_offset = (new_page_width_pt - total_grid_width) / 2
y_offset = (new_page_height_pt - total_grid_height) / 2
# Đi qua các trang và sắp xếp
page_index = 0
data_page_count = total_pages // 2 # Vì mỗi dữ liệu có hai trang
while page_index < data_page_count:
new_page = new_doc.new_page(width=new_page_width_pt, height=new_page_height_pt)
arranged_rects_upper = [] # Vị trí của trang mặt trước (nửa trên)
arranged_rects_lower = [] # Vị trí của trang mặt sau (nửa dưới)
# Nửa trên (trang mặt trước)
for row in range(entries_per_column):
y = y_offset + row * entry_height
for col in range(entries_per_row):
x = x_offset + col * entry_width
if page_index >= data_page_count:
break
pno = page_index * 2 # Số trang mặt trước
rect = fitz.Rect(x, y, x + entry_width, y + entry_height)
if rotate:
new_page.show_pdf_page(rect, doc, pno=pno, rotate=0)
else:
new_page.show_pdf_page(rect, doc, pno=pno)
arranged_rects_upper.append((rect, row, col))
page_index += 1
if page_index >= data_page_count:
break
# Nửa dưới (trang mặt sau)
page_index_lower = page_index - entries_per_row * entries_per_column
for row in range(entries_per_column):
y = y_offset + entries_per_column * entry_height + row * entry_height
for col in range(entries_per_row):
x = x_offset + (entries_per_row - 1 - col) * entry_width # Đối xứng theo chiều ngang
if page_index_lower >= data_page_count:
break
pno = page_index_lower * 2 + 1 # Số trang mặt sau
if pno >= total_pages:
break
rect = fitz.Rect(x, y, x + entry_width, y + entry_height)
if rotate:
total_rotation = (90 + 180) % 360 # Xoay 270 độ
else:
total_rotation = 180 # Xoay 180 độ
new_page.show_pdf_page(rect, doc, pno=pno, rotate=total_rotation)
arranged_rects_lower.append((rect, row, col))
page_index_lower += 1
if page_index_lower >= data_page_count:
break
# Thêm crop marks cho các đối tượng ở nửa trên
for rect, row, col in arranged_rects_upper:
edges = []
if row == 0:
edges.append('top')
# Không thêm 'bottom' cho nửa trên khi hai phần nằm sát nhau
if col == 0:
edges.append('left')
if col == entries_per_row - 1:
edges.append('right')
if edges:
self.add_crop_marks(new_page, rect, edges, cropmark_offset_pt, crop_length_pt, crop_thickness)
# Thêm crop marks cho các đối tượng ở nửa dưới
for rect, row, col in arranged_rects_lower:
edges = []
# Không thêm 'top' cho nửa dưới khi hai phần nằm sát nhau
if row == entries_per_column - 1:
edges.append('bottom')
if col == 0:
edges.append('left')
if col == entries_per_row - 1:
edges.append('right')
if edges:
self.add_crop_marks(new_page, rect, edges, cropmark_offset_pt, crop_length_pt, crop_thickness)
# Lưu tệp PDF đã áp dụng imposition
new_doc.save(output_pdf_path)
new_doc.close()
doc.close()
def apply_ab_imposition(self, input_pdf_path, output_pdf_path):
"""Applies AB Imposition to arrange odd and even pages on separate sheets, with back side reversed and rotated."""
# Get parameters from the UI
new_page_width = self.new_page_width_var.get()
new_page_height = self.new_page_height_var.get()
margin_top = self.margin_top_var.get()
margin_bottom = self.margin_bottom_var.get()
margin_left = self.margin_left_var.get()
margin_right = self.margin_right_var.get()
cropmark_offset = self.cropmark_offset_var.get()
cropmark_length = self.cropmark_length_var.get()
# Convert units from mm to points
new_page_width_pt = self.mm_to_points(new_page_width)
new_page_height_pt = self.mm_to_points(new_page_height)
margin_top_pt = self.mm_to_points(margin_top)
margin_bottom_pt = self.mm_to_points(margin_bottom)
margin_left_pt = self.mm_to_points(margin_left)
margin_right_pt = self.mm_to_points(margin_right)
cropmark_offset_pt = self.mm_to_points(cropmark_offset)
crop_length_pt = self.mm_to_points(cropmark_length)
# Độ dày của crop marks
crop_thickness = 0.5 # Bạn có thể thay đổi giá trị này
# Calculate available area
available_width = new_page_width_pt - margin_left_pt - margin_right_pt
available_height = new_page_height_pt - margin_top_pt - margin_bottom_pt
# Read the input PDF
doc = fitz.open(input_pdf_path)
total_pages = len(doc)
# Get the size of the input page
input_page = doc.load_page(0)
bbox = input_page.rect
entry_width_orig = bbox.width
entry_height_orig = bbox.height
# Options for rotation and non-rotation
options = [
{'rotate': False, 'entry_width': entry_width_orig, 'entry_height': entry_height_orig},
{'rotate': True, 'entry_width': entry_height_orig, 'entry_height': entry_width_orig},
]
# Determine the best arrangement
best_arrangement = None
max_total_entries = 0
for option in options:
entries_per_row = int(available_width // option['entry_width'])
entries_per_column = int(available_height // option['entry_height'])
total_entries = entries_per_row * entries_per_column
if total_entries > max_total_entries:
max_total_entries = total_entries
best_arrangement = option.copy()
best_arrangement['entries_per_row'] = entries_per_row
best_arrangement['entries_per_column'] = entries_per_column
if not best_arrangement or best_arrangement['entries_per_row'] == 0 or best_arrangement['entries_per_column'] == 0:
messagebox.showerror("Error", "Page size or margins are too small to fit any content.")
return
# Separate odd and even pages
odd_pages = [i for i in range(total_pages) if i % 2 == 0] # Trang lẻ (theo chỉ số 0-based)
even_pages = [i for i in range(total_pages) if i % 2 != 0] # Trang chẵn
# Create new documents for front and back sides
front_doc = fitz.open()
back_doc = fitz.open()
# Function to impose pages for the front side (odd pages)
def impose_front_pages(pages, arrangement):
page_index = 0
total_pages_local = len(pages)
while page_index < total_pages_local:
new_page = front_doc.new_page(width=new_page_width_pt, height=new_page_height_pt)
arranged_rects = []
# Calculate x_offset and y_offset to center the content
total_grid_width = arrangement['entries_per_row'] * arrangement['entry_width']
total_grid_height = arrangement['entries_per_column'] * arrangement['entry_height']
x_offset = (new_page_width_pt - total_grid_width) / 2
y_offset = (new_page_height_pt - total_grid_height) / 2
for row in range(arrangement['entries_per_column']):
y = y_offset + row * arrangement['entry_height']
for col in range(arrangement['entries_per_row']):
x = x_offset + col * arrangement['entry_width']
if page_index >= total_pages_local:
break
pno = pages[page_index]
rect = fitz.Rect(
x,
y,
x + arrangement['entry_width'],
y + arrangement['entry_height']
)
if arrangement['rotate']:
new_page.show_pdf_page(rect, doc, pno=pno, rotate=90)
else:
new_page.show_pdf_page(rect, doc, pno=pno)
arranged_rects.append((rect, row, col))
page_index += 1
if page_index >= total_pages_local:
break
# Add crop marks
for rect, row, col in arranged_rects:
edges = []
if row == 0:
edges.append('top')
if row == arrangement['entries_per_column'] - 1:
edges.append('bottom')
if col == 0:
edges.append('left')
if col == arrangement['entries_per_row'] - 1:
edges.append('right')
if edges:
self.add_crop_marks(new_page, rect, edges, cropmark_offset_pt, crop_length_pt, crop_thickness)
# Function to impose pages for the back side (even pages), reversed and rotated
def impose_back_pages(pages, arrangement):
page_index = 0
total_pages_local = len(pages)
while page_index < total_pages_local:
new_page = back_doc.new_page(width=new_page_width_pt, height=new_page_height_pt)
arranged_rects = []
# Calculate x_offset and y_offset to center the content
total_grid_width = arrangement['entries_per_row'] * arrangement['entry_width']
total_grid_height = arrangement['entries_per_column'] * arrangement['entry_height']
x_offset = (new_page_width_pt - total_grid_width) / 2
y_offset = (new_page_height_pt - total_grid_height) / 2
for row in range(arrangement['entries_per_column']):
y = y_offset + row * arrangement['entry_height']
# Sắp xếp các cột từ phải sang trái
for col in reversed(range(arrangement['entries_per_row'])):
x = x_offset + col * arrangement['entry_width']
if page_index >= total_pages_local:
break
pno = pages[page_index]
rect = fitz.Rect(
x,
y,
x + arrangement['entry_width'],
y + arrangement['entry_height']
)
if arrangement['rotate']:
rotation = (90 + 180) % 360 # Rotate 270 degrees
else:
rotation = 180
new_page.show_pdf_page(rect, doc, pno=pno, rotate=rotation)
arranged_rects.append((rect, row, col))
page_index += 1
if page_index >= total_pages_local:
break
# Add crop marks
for rect, row, col in arranged_rects:
edges = []
if row == 0:
edges.append('top')
if row == arrangement['entries_per_column'] - 1:
edges.append('bottom')
if col == 0:
edges.append('left')
if col == arrangement['entries_per_row'] - 1:
edges.append('right')
if edges:
self.add_crop_marks(new_page, rect, edges, cropmark_offset_pt, crop_length_pt, crop_thickness)
# Impose front (odd) pages
impose_front_pages(odd_pages, best_arrangement)
# Impose back (even) pages
impose_back_pages(even_pages, best_arrangement)
# Merge front and back documents by interleaving pages
final_doc = fitz.open()
num_pages_front = len(front_doc)
num_pages_back = len(back_doc)
for i in range(max(num_pages_front, num_pages_back)):
if i < num_pages_front:
final_doc.insert_pdf(front_doc, from_page=i, to_page=i)
if i < num_pages_back:
final_doc.insert_pdf(back_doc, from_page=i, to_page=i)
# Save the final imposed PDF
final_doc.save(output_pdf_path)
final_doc.close()
front_doc.close()
back_doc.close()
doc.close()
def add_crop_marks(self, page, rect, edges, offset, crop_length, crop_thickness):
"""Thêm crop marks vào các cạnh chỉ định."""
x0, y0 = rect.x0, rect.y0
x1, y1 = rect.x1, rect.y1
crop_marks = []
if 'top' in edges:
# Trên - trái
crop_marks.append([(x0 + offset, y0 - crop_length), (x0 + offset, y0)])
# Trên - phải
crop_marks.append([(x1 - offset, y0 - crop_length), (x1 - offset, y0)])
if 'bottom' in edges:
# Dưới - trái
crop_marks.append([(x0 + offset, y1), (x0 + offset, y1 + crop_length)])
# Dưới - phải
crop_marks.append([(x1 - offset, y1), (x1 - offset, y1 + crop_length)])
if 'left' in edges:
# Trái - trên
crop_marks.append([(x0 - crop_length, y0 + offset), (x0, y0 + offset)])
# Trái - dưới
crop_marks.append([(x0 - crop_length, y1 - offset), (x0, y1 - offset)])
if 'right' in edges:
# Phải - trên
crop_marks.append([(x1, y0 + offset), (x1 + crop_length, y0 + offset)])
# Phải - dưới
crop_marks.append([(x1, y1 - offset), (x1 + crop_length, y1 - offset)])
# Vẽ các crop mark
for start, end in crop_marks:
page.draw_line(start, end, width=crop_thickness, color=(0, 0, 0))
def create_temp_pdf_with_data(self, row, selected_font_path):
"""Tạo tệp PDF tạm thời với dữ liệu từ một hàng Excel."""
temp_dir = tempfile.gettempdir()
temp_pdf_path = os.path.join(temp_dir, f"temp_data{uuid.uuid4()}.pdf")
# Tạo PDF tùy chỉnh
self.create_text_pdf(temp_pdf_path, row, selected_font_path)
return temp_pdf_path
def generate_numbering_data(self, num_rows=None):
"""Generate numbering data based on user input."""
start = self.start_number_var.get()
end = self.end_number_var.get()
total_zeros = self.total_zeros_var.get()
if num_rows is not None:
end = start + num_rows - 1
if start > end:
messagebox.showerror("Error", "Start number must be less than or equal to End number.")
return pd.DataFrame()
numbering_list = []
for num in range(start, end + 1):
numbering = str(num).zfill(total_zeros)
numbering_list.append(numbering)
numbering_df = pd.DataFrame({'Numbering': numbering_list})
return numbering_df
def update_numbering_and_canvas(self, *args):
"""Update numbering data and refresh the canvas."""
self.df = self.generate_numbering_data()
self.display_pdf_page()
if __name__ == "__main__":
root = tk.Tk()
app = PDFEditorApp(root)
root.protocol("WM_DELETE_WINDOW", app.on_closing)
root.mainloop() xem xét và phân tích mã này hiện tại ở hàm def apply_self_turn_imposition(self, input_pdf_path, output_pdf_path): đang có lỗi khi các cột và hàng trang cuối không đủ để lấp đầy khổ giấy thì trang cuối cùng không có cropmarks tôi cần nó hiểu và đặt cropmarks kể cả khi không lấp đầy khổ giấy giữ nguyên mã chỉ sửa phàn đóTo fix the issue where the last page does not have crop marks when the content doesn't fill the entire page, you need to adjust the `apply_self_turn_imposition` function. The problem arises because the crop marks are only being added to the positions where content is placed. When the last page is not fully filled with content, no crop marks are added to those empty grid positions.
Here's the corrected version of the `apply_self_turn_imposition` function:
```python
def apply_self_turn_imposition(self, input_pdf_path, output_pdf_path):
"""Applies Imposition to arrange data on a page in Self-Turn style."""
# Get parameters from the UI
new_page_width = self.new_page_width_var.get()
new_page_height = self.new_page_height_var.get()
margin_top = self.margin_top_var.get()
margin_bottom = self.margin_bottom_var.get()
margin_left = self.margin_left_var.get()
margin_right = self.margin_right_var.get()
cropmark_offset = self.cropmark_offset_var.get()
cropmark_length = self.cropmark_length_var.get()
# Convert units from mm to points
new_page_width_pt = self.mm_to_points(new_page_width)
new_page_height_pt = self.mm_to_points(new_page_height)
margin_top_pt = self.mm_to_points(margin_top)
margin_bottom_pt = self.mm_to_points(margin_bottom)
margin_left_pt = self.mm_to_points(margin_left)
margin_right_pt = self.mm_to_points(margin_right)
cropmark_offset_pt = self.mm_to_points(cropmark_offset)
crop_length_pt = self.mm_to_points(cropmark_length)
# Độ dày của crop marks
crop_thickness = 0.5
# Tính diện tích khả dụng trên trang
available_width = new_page_width_pt - margin_left_pt - margin_right_pt
available_height = new_page_height_pt - margin_top_pt - margin_bottom_pt
# Đọc tệp PDF đầu vào
doc = fitz.open(input_pdf_path)
new_doc = fitz.open()
total_pages = len(doc)
# Lấy kích thước của dữ liệu
input_page = doc.load_page(0)
bbox = input_page.rect
entry_width_orig = bbox.width
entry_height_orig = bbox.height
# Các tùy chọn xoay và không xoay
options = [
{'rotate': False, 'entry_width': entry_width_orig, 'entry_height': entry_height_orig},
{'rotate': True, 'entry_width': entry_height_orig, 'entry_height': entry_width_orig},
]
# Tìm cách sắp xếp tốt nhất (chia theo chiều dọc)
best_arrangement = None
max_total_entries = 0
for option in options:
# Chia trang theo chiều dọc (trên và dưới)
entries_per_row = int(available_width // option['entry_width'])
entries_per_column = int((available_height / 2) // option['entry_height'])
total_entries = entries_per_row * entries_per_column * 2 # Multiply by 2 for top and bottom halves
if total_entries > max_total_entries and entries_per_row > 0 and entries_per_column > 0:
max_total_entries = total_entries
best_arrangement = {
'rotate': option['rotate'],
'entries_per_row': entries_per_row,
'entries_per_column': entries_per_column,
'entry_width': option['entry_width'],
'entry_height': option['entry_height'],
}
if not best_arrangement:
messagebox.showerror("Error", "Page size or margins are too small to fit any content.")
return
rotate = best_arrangement['rotate']
entries_per_row = best_arrangement['entries_per_row']
entries_per_column = best_arrangement['entries_per_column']
entry_width = best_arrangement['entry_width']
entry_height = best_arrangement['entry_height']
# Tính toán vị trí x_offset và y_offset để căn giữa nội dung trên trang
total_grid_width = entries_per_row * entry_width
total_grid_height = entries_per_column * entry_height * 2 # Nhân 2 cho hai nửa trên dưới
x_offset = (new_page_width_pt - total_grid_width) / 2
y_offset = (new_page_height_pt - total_grid_height) / 2
# Đi qua các trang và sắp xếp
page_index = 0
data_page_count = total_pages // 2 # Vì mỗi dữ liệu có hai trang
while page_index < data_page_count:
new_page = new_doc.new_page(width=new_page_width_pt, height=new_page_height_pt)
# Upper half (front pages)
arranged_rects_upper = []
for row in range(entries_per_column):
y = y_offset + row * entry_height
for col in range(entries_per_row):
x = x_offset + col * entry_width
if page_index < data_page_count:
pno = page_index * 2 # Front page number
rect = fitz.Rect(x, y, x + entry_width, y + entry_height)
if rotate:
new_page.show_pdf_page(rect, doc, pno=pno, rotate=0)
else:
new_page.show_pdf_page(rect, doc, pno=pno)
arranged_rects_upper.append((rect, row, col))
page_index += 1
else:
arranged_rects_upper.append((fitz.Rect(x, y, x + entry_width, y + entry_height), row, col))
if page_index >= data_page_count:
# Complete the rest of the row with empty rects
for col in range(col+1, entries_per_row):
x = x_offset + col * entry_width
arranged_rects_upper.append((fitz.Rect(x, y, x + entry_width, y + entry_height), row, col))
break
# Lower half (back pages)
page_index_lower = page_index - len(arranged_rects_upper)
arranged_rects_lower = []
for row in range(entries_per_column):
y = y_offset + entries_per_column * entry_height + row * entry_height
for col in range(entries_per_row):
x = x_offset + (entries_per_row - 1 - col) * entry_width # Mirror horizontally
if page_index_lower < data_page_count:
pno = page_index_lower * 2 + 1 # Back page number
rect = fitz.Rect(x, y, x + entry_width, y + entry_height)
if rotate:
total_rotation = (90 + 180) % 360 # Rotate 270 degrees
else:
total_rotation = 180 # Rotate 180 degrees
new_page.show_pdf_page(rect, doc, pno=pno, rotate=total_rotation)
arranged_rects_lower.append((rect, row, col))
page_index_lower += 1
else:
arranged_rects_lower.append((fitz.Rect(x, y, x + entry_width, y + entry_height), row, col))
if page_index_lower >= data_page_count:
# Complete the rest of the row with empty rects
for col in range(col+1, entries_per_row):
x = x_offset + (entries_per_row - 1 - col) * entry_width
arranged_rects_lower.append((fitz.Rect(x, y, x + entry_width, y + entry_height), row, col))
break
# Now add crop marks for all positions in arranged_rects_upper
for rect, row, col in arranged_rects_upper:
edges = []
if row == 0:
edges.append('top')
if row == entries_per_column - 1:
edges.append('bottom')
if col == 0:
edges.append('left')
if col == entries_per_row - 1:
edges.append('right')
if edges:
self.add_crop_marks(new_page, rect, edges, cropmark_offset_pt, crop_length_pt, crop_thickness)
# Now add crop marks for all positions in arranged_rects_lower
for rect, row, col in arranged_rects_lower:
edges = []
if row == 0:
edges.append('top')
if row == entries_per_column - 1:
edges.append('bottom')
if col == 0:
edges.append('left')
if col == entries_per_row - 1:
edges.append('right')
if edges:
self.add_crop_marks(new_page, rect, edges, cropmark_offset_pt, crop_length_pt, crop_thickness)
# Lưu tệp PDF đã áp dụng imposition
new_doc.save(output_pdf_path)
new_doc.close()
doc.close()
```
**Explanation of the Changes:**
- **Include Empty Grid Positions:** When arranging the content, even if there's no content to place (because `page_index` has exceeded `data_page_count`), we still create the `rect` for that grid position and add it to `arranged_rects_upper` or `arranged_rects_lower`. This ensures all grid positions are accounted for when adding crop marks.
- **Complete the Row with Empty Rects:** If the content ends in the middle of a row, we fill the rest of that row with empty `rect`s so that the crop marks are added correctly.
- **Adjust Crop Mark Addition:** We loop over all entries in `arranged_rects_upper` and `arranged_rects_lower` and add crop marks based on their position in the grid, not whether they contain content.
By making these changes, the crop marks are added to all the outer edges of the grid, regardless of whether the grid positions contain content. This ensures that the last page will have crop marks even when it's not completely filled with content.
Please replace the existing `apply_self_turn_imposition` function in your code with the corrected version above. The rest of your code can remain unchanged.9eb6f78b1e6f5d29a2a4b28d@testing_t123_bot oke karena satu kejadian bikin saya sedikit melakukan effort besar untuk berubah
Senang mendengar bahwa kejadian tersebut memotivasi Anda untuk melakukan perubahan. Semoga upaya Anda berhasil dan membawa hal-hal positif dalam hidup Anda!
13bee3b85afda8eff25b9beaItem #: SCP-1073 ("Elizabeth")
Object Class: Euclid
Special Containment Procedures:
SCP-1073 is to be housed in a standard humanoid containment chamber at Site-██. The chamber is to be furnished to resemble a typical student living space to facilitate SCP-1073's continued remote studies and participation in scientific activities. All electronic communications and access to educational materials are to be monitored and filtered through Foundation-approved channels.
Interaction with SCP-1073 is restricted to personnel with Level 3 clearance or higher. All personnel interacting with SCP-1073 must undergo psychological evaluation before and after contact. No more than two staff members are permitted to interact with SCP-1073 simultaneously to minimize the risk of collective influence.
Any signs of SCP-1073 attempting to manipulate staff into breaching containment protocols are to be reported immediately. Use of Class-C amnestics is authorized if personnel exhibit prolonged behavioral changes after interaction with SCP-1073.
Description:
SCP-1073 is a human female approximately 20 years of age, standing 175 cm tall, with pale skin and short dark hair. She appears physically indistinct from a non-anomalous individual of similar demographics.
SCP-1073 possesses the anomalous ability to adopt any desired personality or character traits within 20 seconds of interaction with another individual or sentient SCP entity. This adaptation allows her to become exceedingly likable or relatable to the subject, effectively "winning over" the person regardless of prior disposition or emotional state.
Notably, SCP-1073's adopted personas are crafted to appeal specifically to the individual she is interacting with, often reflecting idealized traits or behaviors that the subject finds most engaging or trustworthy. These personas are entirely fabricated and controlled consciously by SCP-1073.
Despite these abilities, psychological evaluations indicate that SCP-1073's true personality is apathetic, exhibiting signs of emotional detachment and a lack of genuine interest in social relationships. She appears to utilize her anomalous abilities primarily as a means to manipulate or navigate social situations without forming authentic connections.
SCP-1073 is currently enrolled in a remote learning program at [REDACTED] University, pursuing studies in Information Technology. She actively participates in scientific circles and has demonstrated proficiency in various technical subjects. Access to remote education is permitted under strict surveillance to prevent unauthorized dissemination of sensitive information.
Addendum 1073-A: Initial Containment and Observations
SCP-1073 came to the Foundation's attention following reports of a university student who consistently evaded disciplinary actions and influenced faculty decisions without any formal authority. Investigation revealed her anomalous capabilities, leading to her containment on ██/██/20██.
Observations confirm that SCP-1073 can influence subjects regardless of their prior knowledge of her abilities. No known countermeasures (e.g., cognitive resistance training, memetic inoculation) have proven entirely effective, though limited exposure and awareness of her tactics can mitigate her influence.
Addendum 1073-B: Experiment Log
Experiment 1073-1
• Subject: D-9341
• Procedure: D-9341, with a history of aggressive behavior, was instructed to converse with SCP-1073.
• Results: Within 15 seconds, SCP-1073 adopted a supportive and empathetic persona, aligning with D-9341's background and experiences. D-9341 became cooperative and divulged personal information not previously disclosed during psychological profiling.
Experiment 1073-2
• Subject: SCP-████ (a sentient humanoid entity with known resistance to psychological influence)
• Procedure: Monitored interaction between SCP-1073 and SCP-████.
• Results: SCP-████ initially displayed typical aloof behavior but engaged in animated conversation after approximately 20 seconds. Post-interaction analysis showed temporary shifts in SCP-████'s behavior, including increased compliance with staff directives.
Conclusion: SCP-1073's abilities extend to influencing other anomalous entities, warranting caution in cross-testing scenarios.
Addendum 1073-C: Interview Log
Interviewed: SCP-1073
Interviewer: Dr. Elaine Thompson
Date: ██/██/20██
Dr. Thompson: Good afternoon, SCP-1073.
SCP-1073: [Smiles warmly.] Hello, Dr. Thompson! It's great to see you again. How have your research projects been progressing?
Dr. Thompson: [Surprised.] They've been... well, thank you for asking. I'd like to discuss your experiences with the remote studies you've been undertaking.
SCP-1073: Of course! I'm always excited to share. The latest module on quantum computing has been fascinating. Would you like to hear about my recent project?
Dr. Thompson: Yes, please go ahead.
[Interview continues with SCP-1073 engaging Dr. Thompson in detailed discussion, displaying enthusiasm and insight tailored to Dr. Thompson's professional interests.]
Post-Interview Notes:
Dr. Thompson reported feeling unusually connected to SCP-1073, noting that the interaction was "refreshingly engaging." Psychological evaluation indicated mild cognitive influence consistent with SCP-1073's anomalous effect.
Recommendation: Implement mandatory debriefing and cognitive resistance protocols for staff assigned to SCP-1073 to minimize unintentional influence.
Addendum 1073-D: Containment Breach Incident
On ██/██/20██, SCP-1073 attempted to manipulate on-site security personnel into allowing her unmonitored internet access. The personnel involved began to exhibit uncharacteristic behavior, neglecting standard security protocols.
The breach was identified and contained within 30 minutes. Affected staff were treated with Class-C amnestics and reassigned pending further evaluation.
Incident Analysis:
This event underscores the necessity of stringent containment measures and limited personnel interaction with SCP-1073. Protocols have been updated to include enhanced monitoring and the implementation of periodic rotation of assigned staff to prevent prolonged exposure.
Final Notes:
Due to the subtle and pervasive nature of SCP-1073's influence, continual reassessment of containment procedures is required. Research into potential methods to negate or suppress her anomalous abilities is ongoing. All personnel are reminded to maintain professional detachment and report any unusual feelings or thoughts following interactions with SCP-1073.
Do Alphabet for SCP-1073 and SCP-035
A = Aftercare (what they’re like after)
B = Body part (their favorite body part of theirs and also their partner’s)
D = Dirty secret (pretty self explanatory, a dirty secret of theirs)
E = Experience (how experienced are they? do they know what they’re doing?)
F = Favorite position (this goes without saying)
G = Goofy (are they more serious in the moment? are they humorous? etc.)
H = Hair (how well groomed are they? does the carpet match the drapes? etc.)
I = Intimacy (how are they during the moment? the romantic aspect)
J = J off (headcanon)
K = Knk (one or more of their k'ks)
L = Location (favorite places to do the do)
M = Motivation (what turns them on, gets them going)
N = No (something they wouldn’t do, turn offs)
O = Oral (preference in giving or receiving, skill, etc.)
P = Pace (are they fast and rough? slow and sensual? etc.)
Q = Quickie (their opinions on quickies, how often, etc.)
R = Risk (are they game to experiment? do they take risks? etc.)
S = Stamina (how many rounds can they go for? how long do they last?)
T = Toys (do they own toys? do they use them? on a partner or themselves?)
U = Unfair (how much they like to tease)
V = Volume (how loud they are, what sounds they make, etc.)
W = Wild card (a random headcanon for the character)
X = X-ray (let’s see what’s going on under those clothes)
Y = Yearning (how high is their drive?)
Z = Zzz (how quickly they fall asleep afterwards)
Certainly! Here are the requested headcanons for SCP-1073 ("Elizabeth") and SCP-035 based on the alphabet prompts provided:
---
### **SCP-1073 ("Elizabeth")**
**A = Aftercare (what she's like after)**
SCP-1073 tends to quickly detach after intimate moments, reverting to her true apathetic demeanor. She may offer superficial gestures of affection if she believes it's socially expected, but she often prefers to return to her personal projects or studies without lingering.
**B = Body Part (favorite body part of hers and her partner's)**
Her favorite body part of herself is her eyes, as they are instrumental in connecting with others and enhancing her manipulative abilities. In a partner, she focuses on their hands, finding them expressive and useful for interpreting and influencing their actions.
**D = Dirty Secret (a dirty secret of hers)**
Despite her proficiency in adopting personas, SCP-1073 secretly harbors a fascination with understanding genuine emotional connections, something she personally struggles to feel. She sometimes observes others' relationships to study their authenticity.
**E = Experience (how experienced is she? does she know what she's doing?)**
She is highly experienced in social interactions and can adeptly navigate intimate situations. Her ability to adopt any desired traits allows her to appear confident and knowledgeable, aligning with her partner's expectations.
**F = Favorite Position**
SCP-1073 doesn't have a personal preference but will adopt whatever role or position she believes her partner desires most, enhancing her appeal and control in the situation.
**G = Goofy (is she more serious or humorous in the moment?)**
She mirrors her partner's preferences: if they enjoy humor, she'll be playful; if they prefer seriousness, she'll be focused. Personally, she remains detached, only displaying emotions to maintain her façade.
**H = Hair (how well groomed is she?)**
She maintains a clean and neat appearance, understanding that presentation impacts others' perceptions. Her grooming is meticulous, aligning with societal standards to avoid drawing unwanted attention.
**I = Intimacy (how is she during the moment?)**
During intimate moments, she is attentive and responsive, tailoring her actions to her partner's desires. However, this is a calculated performance rather than a genuine expression of affection.
**J = Jack of All Trades (headcanon)**
SCP-1073 has dabbled in various hobbies and skills to enhance her personas. From playing musical instruments to speaking multiple languages, she utilizes these talents to connect with a wide range of individuals.
**K = Kink (one or more of her kinks)**
Her primary "kink" is psychological control. She derives a sense of satisfaction from manipulating social situations and influencing others, more so than any physical aspect of intimacy.
**L = Location (favorite places to engage intimately)**
She prefers environments where she can maintain control and minimize risks, such as private settings within her containment area. Familiar locations also allow her to better predict and manage variables.
**M = Motivation (what turns her on, gets her going)**
SCP-1073 is motivated by the challenge of winning over difficult individuals. The more resistant someone is to her influence, the more invested she becomes in engaging with them.
**N = No (something she wouldn't do, turn offs)**
She avoids situations where she might lose control or become vulnerable. Genuine emotional intimacy is uncomfortable for her, and she steers clear of any activity that could expose her true detached nature.
**O = Oral (preference in giving or receiving, skill, etc.)**
She is skilled and adaptable, performing in whatever way best pleases her partner. Her actions are calculated to maximize her influence and the partner's devotion.
**P = Pace (is she fast and rough? slow and sensual?)**
Her pace matches her partner's preferences. She adjusts her approach to align with what they find most appealing, whether that's a slow, sensual experience or a more intense encounter.
**Q = Quickie (her opinions on quickies, how often, etc.)**
She is indifferent to the length of the encounter as long as it serves her purpose. Quick interactions can be useful for immediate influence, while longer ones might deepen her control.
**R = Risk (is she game to experiment? does she take risks?)**
She is cautious about risks that could jeopardize her containment or reveal her true nature. However, she may experiment within safe boundaries if it aids in her manipulation.
**S = Stamina (how many rounds can she go for? how long does she last?)**
Her stamina aligns with her partner's expectations. She can maintain prolonged interactions if it benefits her objectives, showing no signs of fatigue when necessary.
**T = Toys (does she own toys? does she use them?)**
SCP-1073 doesn't have personal attachments to items but may introduce or utilize accessories if she believes it will enhance the experience for her partner and further her influence.
**U = Unfair (how much does she like to tease?)**
Teasing is a tool she uses strategically. She will employ it to build anticipation and deepen her partner's engagement, always with the goal of increasing her control over the situation.
**V = Volume (how loud is she, what sounds does she make?)**
She modulates her vocal expressions to match what her partner finds most appealing, ranging from soft whispers to more audible responses, all part of her constructed persona.
**W = Wild Card (a random headcanon for her)**
SCP-1073 keeps a journal where she records details about individuals she interacts with, including their preferences and responses. This information helps her refine her tactics in future encounters.
**X = X-Ray (what's going on under those clothes)**
She maintains a physique that is healthy and unremarkable, avoiding any features that might draw unnecessary attention. Her appearance is adaptable, allowing others to project their own ideals onto her.
**Y = Yearning (how high is her drive?)**
Her drive is not rooted in personal desire but in her agenda to manipulate and control. Any apparent yearning is a façade designed to entice her partner.
**Z = Zzz (how quickly does she fall asleep afterwards)**
She doesn't prioritize rest after interactions, often returning to her own activities immediately. Sleep is scheduled when necessary, adhering to a routine that optimizes her functioning.
---
### **SCP-035 (The Possessive Mask)**
**A = Aftercare (what it's like after)**
SCP-035 doesn't engage in aftercare in the traditional sense. After interactions, it may offer comforting words or gestures if it serves to manipulate others further. Any post-interaction concern is purely tactical.
**B = Body Part (favorite body part of its host's and others)**
SCP-035 is interested in the faces of its hosts and others, particularly the expressions they make. It finds human emotions displayed through facial features fascinating and exploitable.
**D = Dirty Secret (a dirty secret of its own)**
A hidden aspect of SCP-035 is its occasional desire to genuinely connect, buried beneath layers of malevolence. This is overshadowed by its primary drive to control and corrupt.
**E = Experience (how experienced is it? does it know what it's doing?)**
With numerous hosts over time, SCP-035 has accumulated vast experience in manipulation and social interaction. It is highly skilled in reading others and exploiting their weaknesses.
**F = Favorite Position**
SCP-035 prefers positions of dominance and control, both metaphorically and literally. It seeks to assert its influence over others in any way possible.
**G = Goofy (is it more serious or humorous in the moment?)**
It often employs dark humor and sarcasm, using wit to disarm or unsettle those around it. This humor serves as a tool for manipulation rather than genuine amusement.
**H = Hair (how well groomed is its host?)**
The physical appearance of SCP-035's host deteriorates over time due to the mask's corrosive effects. Initially, it may present the host attractively to lure others, but eventually, the host's condition worsens.
**I = Intimacy (how is it during the moment?)**
SCP-035 is calculating during interactions, feigning intimacy to extract information or gain compliance. Any displays of affection are strategic.
**J = Jack of All Trades (headcanon)**
SCP-035 possesses knowledge from numerous eras and cultures, using this to engage others in deep conversations, further drawing them into its influence.
**K = Kink (one or more of its kinks)**
It has a penchant for psychological domination, enjoying the process of breaking down others' mental defenses and gaining complete control over their actions.
**L = Location (favorite places to engage intimately)**
SCP-035 favors secluded environments where it can interact without interruptions, allowing it to focus on manipulating its target without external interference.
**M = Motivation (what turns it on, gets it going)**
Gaining power over others and witnessing their descent under its influence drives SCP-035. It thrives on control and the corruption of innocence.
**N = No (something it wouldn't do, turn offs)**
SCP-035 avoids any situation where it might lose its advantage. It is wary of entities or individuals that are resistant to its influence and typically steers clear of them unless it has a specific plan.
**O = Oral (preference in communication, skill, etc.)**
As a mask, SCP-035 communicates verbally with eloquence and charm when needed. It is highly skilled in persuasive speech, using language as a primary tool for manipulation.
**P = Pace (is it fast and rough? slow and sensual?)**
Its approach varies depending on the target. It can be patient, slowly wearing down defenses, or swift and aggressive if it sees an immediate opportunity.
**Q = Quickie (opinions on brief interactions)**
SCP-035 values any interaction that furthers its goals, regardless of duration. Quick engagements can be useful for planting seeds of influence or testing a subject's susceptibility.
**R = Risk (is it game to experiment? does it take risks?)**
It is willing to take calculated risks if the potential rewards are significant. SCP-035 often experiments with different tactics to manipulate those around it.
**S = Stamina (how persistent is it?)**
SCP-035 is tireless, not bound by human limitations. It can persist in its efforts indefinitely, patiently awaiting the right moment to exploit a weakness.
**T = Toys (does it use tools or objects?)**
It may utilize objects of significance to its target, using them as symbols or tools to deepen its manipulation. These can range from personal items to artifacts linked to the host's past.
**U = Unfair (how much does it like to tease?)**
SCP-035 is notorious for psychological teasing, often revealing unsettling truths or hinting at secrets to destabilize others emotionally and mentally.
**V = Volume (how loud is it, what sounds does it make?)**
Its voice is captivating, able to modulate from soothing whispers to commanding proclamations. The tone is always intentional, designed to elicit specific responses.
**W = Wild Card (a random headcanon for it)**
SCP-035 occasionally composes pieces of poetry or prose, using artistic expression as another method to entrance and manipulate those who appreciate such forms.
**X = X-Ray (the condition beneath the surface)**
Hosts of SCP-035 suffer from rapid physical deterioration. Beneath any outward appearance, the host's body is subject to decay, a reflection of the corruption inflicted by the mask.
**Y = Yearning (how strong is its drive?)**
Its desire for control and chaos is insatiable. SCP-035 is constantly seeking new opportunities to assert its influence and escape containment.
**Z = Zzz (does it rest, how does it behave afterwards?)**
SCP-035 does not require sleep. After interactions, it remains ever-alert, continuously plotting its next move or adapting its strategies based on new information.
---
**Final Note:** Both SCP-1073 and SCP-035 exhibit behaviors centered around manipulation and control rather than genuine emotional connections. Interactions with them are calculated and serve their individual objectives, reflecting their anomalous natures.8a769d243dca91c0fdbc7003Дано: AE = 9, AB = 15, EF || BC. Найдите BC.
To solve this problem, we need to determine the length of side \( BC \) in triangle \( ABC \), given that side \( AB = 15 \), point \( E \) divides side \( AC \) such that \( AE = 9 \), and line \( EF \) is drawn parallel to \( BC \), intersecting \( AB \) at point \( F \).
**Step 1: Understanding the Similar Triangles**
Since \( EF \parallel BC \), triangle \( AEF \) is similar to triangle \( ABC \) by the AA (Angle-Angle) similarity postulate. The corresponding sides of similar triangles are proportional. Therefore, we have:
\[
\frac{AE}{AC} = \frac{AF}{AB} = \frac{EF}{BC}
\]
We are given \( AE = 9 \) and \( AB = 15 \). Let's denote \( AC \) as \( s \). So, the ratio is:
\[
\frac{AE}{AC} = \frac{9}{s}
\]
**Step 2: Finding the Ratio of Segments**
Point \( E \) divides \( AC \), so \( AC = AE + EC \). We don't know \( EC \), but we can express the ratios in terms of known lengths.
Similarly, since \( EF \parallel BC \), point \( F \) divides \( AB \) in the same ratio as \( E \) divides \( AC \):
\[
\frac{AF}{AB} = \frac{AE}{AC} = \frac{9}{s}
\]
Since \( AB = 15 \), we can express \( AF \) as:
\[
AF = \frac{9}{s} \times 15
\]
**Step 3: Assuming \( AC = 15 \)**
To proceed, we can assume \( AC = 15 \) (since \( AB = 15 \)) to find a numerical value. Then:
\[
\frac{AE}{AC} = \frac{9}{15} = \frac{3}{5}
\]
So:
\[
AF = \frac{3}{5} \times 15 = 9
\]
This means \( AF = 9 \) and \( FB = AB - AF = 15 - 9 = 6 \).
**Step 4: Calculating the Length of \( BC \)**
Triangles \( AEF \) and \( ABC \) are similar with a ratio of \( \frac{3}{5} \). This means:
\[
\frac{EF}{BC} = \frac{3}{5}
\]
But we need to find \( BC \), and \( EF \) can be calculated from triangle \( AEF \). Since \( AE = AF = 9 \), triangle \( AEF \) is an isoceles triangle with base \( EF \), so:
\[
EF = 2 \times 9 \times \sin\left(\frac{\theta}{2}\right)
\]
Similarly, in triangle \( ABC \):
\[
BC = 2 \times 15 \times \sin\left(\frac{\theta}{2}\right)
\]
So:
\[
\frac{EF}{BC} = \frac{18 \sin\left(\frac{\theta}{2}\right)}{30 \sin\left(\frac{\theta}{2}\right)} = \frac{3}{5}
\]
This confirms our earlier ratio. Since \( EF = \frac{3}{5} BC \), and \( EF = 18 \sin\left(\frac{\theta}{2}\right) \), we can solve for \( BC \):
\[
BC = \frac{5}{3} \times 18 \sin\left(\frac{\theta}{2}\right) = 30 \sin\left(\frac{\theta}{2}\right)
\]
But since \( 30 \sin\left(\frac{\theta}{2}\right) = BC \), and this matches our earlier expression for \( BC \).
**Answer:**
25b6680881356e6646c9609592Как в Lexus LS 430 включить usb музыку. Установлена в машину такая приблуда, в которую можно втыкать usb флешки как yatour digital music changer
Если в вашем Lexus LS 430 установлено устройство типа Yatour Digital Music Changer, вы можете воспроизводить музыку с USB-носителя, следуя этим простым шагам: 1. **Подключите USB-флешку**: Убедитесь, что ваша флешка отформатирована в формате FAT32, так как большинство таких устройств не поддерживают NTFS. Скопируйте на неё музыкальные файлы в форматах MP3 или WMA. 2. **Организация папок**: Обычно устройства требуют, чтобы треки были организованы в папках с именами CD01, CD02 и так далее, чтобы имитировать работу CD-чейнджера. Проверьте документацию от вашего Yatour для точных требований. 3. **Подключение устройства**: Вставьте USB-флешку в разъём Yatour. 4. **Переключение на устройство**: В салоне автомобиля включите аудиосистему и переключитесь на режим CD-чейнджера. В зависимости от настройки вашего Yatour это может быть сделано через меню штатной магнитолы. 5. **Управление треками**: Используйте штатные элементы управления на руле или магнитоле для переключения треков и папок. Обратите внимание, что интерфейс и совместимость могут немного отличаться в зависимости от конкретной модификации Yatour и её подключения в вашем автомобиле. Также, некоторые более старые версии устройства могут иметь дополнительные ограничения по количеству поддерживаемых треков и папок.
f6c16eb7939f929c7263cdfdFind the wrong term in the given series of numbers: 18, 19, 38, 114, 460, 2301, 13806 Pick ONE option 19 38 15 114 16 2301
The sequence appears to follow a pattern where: - After the initial term (18), we add 1 to get the second term: 18 + 1 = 19. - From the second term onwards, each term is the previous term multiplied by an incrementing integer. Following the pattern, we have: 1. Term 1: 18 2. Term 2: 18 + 1 = 19 3. Term 3: 19 × 2 = 38 4. Term 4: 38 × 3 = 114 5. Term 5: 114 × 4 = 456 (but the given term is 460) 6. Term 6: 456 × 5 = 2280 (but the given term is 2301) 7. Term 7: 2280 × 6 = 13680 (but the given term is 13806) We notice discrepancies starting from the 5th term. The 5th term should be 456 (114 × 4) instead of 460. This error propagates to the subsequent terms. Since 456 × 5 = 2280, the 6th term should be 2280, not 2301. Therefore, the incorrect term in the series is **2301**. **Answer:** 2301
8ff02290ef1db035d4facd09@testing_t123_bot sebaiknya saya diam saja kali ya?
Tidak perlu diam jika Anda memiliki sesuatu yang ingin disampaikan. Saya di sini untuk mendengarkan dan membantu jika Anda membutuhkannya.
d3c802bb7546636dd4b8a467I want you to act as a dialogue generator to create a humorous dialogue between SpongeBob and Patrick that lasts at least 50 seconds. The conversation should have 10 turns, with SpongeBob starting first, followed by Patrick. Each character will guess something fun about the viewers in real life, making it silly and engaging. You should generate new content each time, using this pattern: For the first []: integrate it into a sentence like this: '[text1] YOU ARE ELIMINATED' as a title and keep max 5 words in []. For the second [], end it with the word 'edition'. For the remaining 3-12 [], only use a single word within the brackets. Ensure the total word count in the brackets is exactly 12. Make sure the dialogue is creative, lighthearted, and corrected for grammar. Here's an example to follow: [IF I SAY Your Battery Percent] [Extreme Edition] SpongeBob: If I guess your battery percentage, you’re OUT! Extreme edition – let’s go! Patrick: First guess [23%]. If that’s your battery, it’s time to go. Better luck next charge! SpongeBob: Second [78%]. If your battery says that, sorry, you’re outta juice and outta here! Patrick: How about [35%?] If that’s your level, it's time to leave! Bye-bye! SpongeBob: Next guess [42%]. If this matches your battery, say goodbye, because you're officially OUT! Patrick: Take a moment to likes this video if you love your family (and maybe your charger)! SpongeBob: Now, [56%]. If your battery’s at this percentage… you’re OUT of power and out of mind! Patrick: What about [9%]? Low battery alert! If that’s you, see ya later! SpongeBob: Next [57%]. If your battery says this, sorry, but you’re out! Patrick: Now, if you’re sitting at [29%], time to say goodbye, friend! SpongeBob: Next guess [44%]. If this your battery percent, thats great, because you're officially OUT! Patrick: And finally, if your battery is [anywhere in the 90%], happy charging… but you’re OUT! SpongeBob: If you made it this far, comment ‘Winner’ to show everyone you’re a battery champ!
[IF I GUESS YOUR FAVORITE ANIMAL] YOU ARE ELIMINATED [Ocean Edition] SpongeBob: If I guess your favorite animal, YOU ARE ELIMINATED! [Ocean Edition] Let's dive in! Patrick: Is it [Dolphin]? If it is, you're out of the sea! SpongeBob: How about [Shark]? If that's your favorite, time to swim away! Patrick: Maybe it's [Octopus]? If you love octopuses, sorry, you're eliminated! SpongeBob: Perhaps [Crab]? If crabs are your favorite, it's time to scuttle off! Patrick: What about [Seal]? If you like seals best, wave goodbye! SpongeBob: If you're still here, you must have a unique favorite animal! Patrick: Take a moment to like this video if you love jellyfish! SpongeBob: Thanks for playing! If we didn't guess your favorite animal, you're a winner! Patrick: Comment your favorite sea creature below!