diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb23b09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +output.pdf +captures/ +dist/ +build/ \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2419ad5 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11.9 diff --git a/README.md b/README.md index b31bd14..2bb378d 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,16 @@ -# Template -This project is a code for ~ +# Ebook Snipping Tool +This project is a code for Ebook snipping and PDF converting ## Prerequisites -- foo -- bar - - ... +- Python 3.11.9 +- pip + - mss + - pyautogui + - opencv-python + - pyqt5 ## Installation 1. Clone this repository to your local machine. -2. Install dependencies using ~ - ```batch - npm install - ``` - -## Usage -To run the script, execute the following command: -```batch -foobar -``` ## Contributing Feel free to contribute to this project by opening issues or pull requests. Any feedback or suggestions are welcome! diff --git a/__pycache__/overlay.cpython-312.pyc b/__pycache__/overlay.cpython-312.pyc new file mode 100644 index 0000000..ebfaba6 Binary files /dev/null and b/__pycache__/overlay.cpython-312.pyc differ diff --git a/__pycache__/widget.cpython-311.pyc b/__pycache__/widget.cpython-311.pyc new file mode 100644 index 0000000..d4358d9 Binary files /dev/null and b/__pycache__/widget.cpython-311.pyc differ diff --git a/__pycache__/widget.cpython-312.pyc b/__pycache__/widget.cpython-312.pyc new file mode 100644 index 0000000..e1adae1 Binary files /dev/null and b/__pycache__/widget.cpython-312.pyc differ diff --git a/assets/icon.ico b/assets/icon.ico new file mode 100644 index 0000000..0cc5a2c Binary files /dev/null and b/assets/icon.ico differ diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..22f8c4f Binary files /dev/null and b/assets/icon.png differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..3fa5499 --- /dev/null +++ b/main.py @@ -0,0 +1,260 @@ +import os +import sys +from utils.utils import resource_path +from PyQt5.QtWidgets import * +from PyQt5 import uic, QtGui, QtCore +from widget import SnippingWidget +from functools import partial +import mss +from PIL import Image +import pyautogui +import cv2 +import numpy as np + + +""" +교보ebook 응용프로그램 전용 +""" + + +class EbookSnipper(QMainWindow): + def __init__(self): + super().__init__() + + os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" + self.capture_dir = "./captures" + + uic.loadUi(resource_path("./ui/MainWindow.ui"), self) + self.coords = { + "x": 0, + "y": 0, + "w": 0, + "h": 0, + "btn_x": 0, + "btn_y": 0, + } + self.params = {"delay": 0, "pages": 0} + + # Icon Setting + self.icon = QtGui.QIcon(resource_path("./assets/icon.ico")) + if self.icon.isNull(): + print("Failed to load icon") + self.setWindowIcon(self.icon) + + self.widget = SnippingWidget(self) + self.current_page = 0 + self.is_snipping = False + + # Initialize + self._initUi() + self.close_spash_screen() + + def _initUi(self): + self.params["delay"] = self.spinBox_delay.value() + self.params["pages"] = self.spinBox_pages.value() + self.btn_coords.clicked.connect(lambda: self.start_setSnippingArea()) + self.btn_nextbtn.clicked.connect(lambda: self.start_setNextBtnCoord()) + for key, spinbox in { + "x": self.spinBox_x, + "y": self.spinBox_y, + "w": self.spinBox_w, + "h": self.spinBox_h, + "btn_x": self.spinBox_btn_x, + "btn_y": self.spinBox_btn_y, + }.items(): + spinbox.valueChanged.connect(partial(self.coords_updownbutton_event, key)) + for key, spinbox in { + "delay": self.spinBox_delay, + "pages": self.spinBox_pages, + }.items(): + spinbox.valueChanged.connect(partial(self.params_updownbutton_event, key)) + + self.btn_start.clicked.connect(self.start_snipping) + self.btn_stop.clicked.connect(self.stop_snipping) + QShortcut(QtGui.QKeySequence("Alt+F10"), self, self.start_snipping) + QShortcut(QtGui.QKeySequence("Alt+F11"), self, self.stop_snipping) + + """ + Pyinstaller Splash Screen Terminator + """ + + def close_spash_screen(self): + if hasattr(sys, "_MEIPASS"): + import pyi_splash # type: ignore + + pyi_splash.close() + + """ + Snipping Area Mode + """ + + def start_setSnippingArea(self): + self.widget.rectMode = True + self.widget.showFullScreen() + + def update_rect_coords(self, coords=None): + self.spinbox_blocksignal(True) + if coords == None: + coords = self.coords + if self.spinBox_x.value() != coords["x"]: + self.spinBox_x.setValue(coords["x"]) + if self.spinBox_y.value() != coords["y"]: + self.spinBox_y.setValue(coords["y"]) + if self.spinBox_w.value() != coords["w"]: + self.spinBox_w.setValue(coords["w"]) + if self.spinBox_h.value() != coords["h"]: + self.spinBox_h.setValue(coords["h"]) + self.spinbox_blocksignal(False) + + """ + Next Button Coord Mode + """ + + def start_setNextBtnCoord(self): + self.widget.rectMode = False + self.widget.showFullScreen() + + def update_btn_coord(self, coords=None): + self.spinbox_blocksignal(True) + if coords == None: + coords = self.coords + if self.spinBox_btn_x.value() != coords["btn_x"]: + self.spinBox_btn_x.setValue(coords["btn_x"]) + if self.spinBox_btn_y.value() != coords["btn_y"]: + self.spinBox_btn_y.setValue(coords["btn_y"]) + self.spinbox_blocksignal(True) + + """ + Event + """ + + def coords_updownbutton_event(self, key, value): + self.coords[key] = value + print(self.coords) + + def params_updownbutton_event(self, key, value): + self.params[key] = value + print(self.params) + + def spinbox_blocksignal(self, boolean): + # if boolean == True: print(f"Before: {self.coords}") + # else: print(f"After: {self.coords}") + self.spinBox_x.blockSignals(boolean) + self.spinBox_y.blockSignals(boolean) + self.spinBox_w.blockSignals(boolean) + self.spinBox_h.blockSignals(boolean) + self.spinBox_btn_x.blockSignals(boolean) + self.spinBox_btn_y.blockSignals(boolean) + + """ + Snipping Process + """ + + def stop_snipping(self): + if self.is_snipping: + self.is_snipping = False + print("Snipping has been stopped") + else: + return + + def start_snipping(self): + if self.is_snipping: + print("Already processing") + return + print("Start snipping") + self.is_snipping = True + self.current_page = 0 + self.capture_next_page() + + def capture_next_page(self): + if self.current_page >= self.params["pages"]: + self.current_page = 0 + self.is_snipping = False + print("Snipping complete") + result = QMessageBox.question( + self, + "캡쳐 완료", + f"{self.params['pages']} 페이지 캡쳐가 완료되었습니다. PDF로 변환 하시겠습니까?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No, # 기본 선택 버튼 + ) + + if result == QMessageBox.Yes: + self.images_to_pdf() + return + + if not self.is_snipping: + QMessageBox.information( + self, + "캡쳐 중단", + f"캡쳐가 중단되었습니다. [{self.current_page}/{self.params['pages']}]", + ) + return + + # capture + self.capture() + + # click next button + pyautogui.click(x=self.coords["btn_x"], y=self.coords["btn_y"]) + + # next page process + self.current_page += 1 + QtCore.QTimer.singleShot(self.params["delay"] * 1000, self.capture_next_page) + + def capture(self): + with mss.mss() as sct: + monitor = { + "left": self.coords["x"], + "top": self.coords["y"], + "width": self.coords["w"], + "height": self.coords["h"], + } + + screenshot = sct.grab(monitor) + img = np.array(screenshot) + img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR) # BGRA → BGR 변환 + + # ✅ 이미지 저장 + if not os.path.exists(self.capture_dir): + os.makedirs(self.capture_dir) + cv2.imwrite(f"{self.capture_dir}/{self.current_page}.png", img) + print(f"> Page {self.current_page + 1} has been captured") + + def images_to_pdf(self): + image_files = [ + f + for f in os.listdir(self.capture_dir) + if f.lower().endswith((".png", ".jpg", ".jpeg")) + ] + image_files.sort() + + if not image_files: + QMessageBox.warning( + self, + "이미지 없음", + f"변환할 이미지가 없습니다.", + ) + return + + first_image = Image.open( + os.path.join(self.capture_dir, image_files[0]) + ).convert("RGB") + + image_list = [] + for file in image_files[1:]: + img = Image.open(os.path.join(self.capture_dir, file)).convert("RGB") + image_list.append(img) + + first_image.save("./output.pdf", save_all=True, append_images=image_list) + QMessageBox.information( + self, + "PDF 변환 완료", + f"PDF 변환이 완료되었습니다.", + ) + + +if __name__ == "__main__": + app = QApplication(sys.argv) + main_window = EbookSnipper() + main_window.show() + sys.exit(app.exec_()) diff --git a/main.spec b/main.spec new file mode 100644 index 0000000..2ad7f2e --- /dev/null +++ b/main.spec @@ -0,0 +1,47 @@ +# -*- mode: python ; coding: utf-8 -*- +import os +import sys + +program_name = 'Ebook Snipper' +icon_path = os.path.abspath('./assets/icon.ico') + +a = Analysis( + ['main.py'], + pathex=['.'], + binaries=[], + datas=[ + ('ui/mainWindow.ui', 'ui'), + ('assets/icon.ico', 'assets'), + ], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) + +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name=program_name, + debug=True, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=icon_path, +) diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui new file mode 100644 index 0000000..e88ddcb --- /dev/null +++ b/ui/mainwindow.ui @@ -0,0 +1,438 @@ + + + MainWindow + + + + 0 + 0 + 340 + 436 + + + + + 340 + 0 + + + + Ebook Snipping Tool + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + 1. 영역 지정 + + + + 4 + + + 4 + + + 4 + + + 4 + + + 3 + + + + + 캡쳐 영역 설정 (취소: esc) + + + + + + + + + + 2. 페이지 넘김 버튼 위치 지정 + + + + 4 + + + 4 + + + 4 + + + 4 + + + 3 + + + + + 버튼 위치 설정 + + + + + + + + + + 3. 파라미터 지정 + + + + 4 + + + 4 + + + 4 + + + 4 + + + 3 + + + + + 캡쳐 영역 + + + true + + + + 4 + + + 4 + + + 4 + + + 4 + + + 3 + + + + + + + + + X: + + + + + + + 20000 + + + + + + + + + + + Y: + + + + + + + 20000 + + + + + + + + + + + Width: + + + + + + + 20000 + + + + + + + + + + + Height: + + + + + + + 20000 + + + + + + + + + + + + + + 버튼 좌표 + + + true + + + + 4 + + + 4 + + + 4 + + + 4 + + + 3 + + + + + + + + + X: + + + + + + + 20000 + + + + + + + + + + + Y: + + + + + + + 20000 + + + + + + + + + + + + + + 조건 + + + true + + + + 4 + + + 4 + + + 4 + + + 4 + + + 3 + + + + + + + + + 딜레이 시간(sec) + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + 20000 + + + 2 + + + + + + + + + + + 페이지 수 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + 20000 + + + + + + + + + + + + + + + + + 4. 캡쳐 + + + + 4 + + + 4 + + + 4 + + + 4 + + + 3 + + + + + + + 시작 (alt + F10) + + + + + + + 중지 (alt + F11) + + + + + + + + + + + + + + + + + 0 + 0 + 340 + 21 + + + + false + + + + + spinBox_x + spinBox_y + spinBox_w + spinBox_h + spinBox_delay + spinBox_pages + btn_start + btn_stop + + + + diff --git a/utils/__pycache__/utils.cpython-311.pyc b/utils/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..a9eb533 Binary files /dev/null and b/utils/__pycache__/utils.cpython-311.pyc differ diff --git a/utils/__pycache__/utils.cpython-312.pyc b/utils/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000..66712f8 Binary files /dev/null and b/utils/__pycache__/utils.cpython-312.pyc differ diff --git a/utils/utils.py b/utils/utils.py new file mode 100644 index 0000000..3a6b013 --- /dev/null +++ b/utils/utils.py @@ -0,0 +1,70 @@ +import os +import sys +import subprocess +from PIL import Image + +def resource_path(relative_path): # for Pyinstaller + if hasattr(sys, '_MEIPASS'): + return os.path.join(sys._MEIPASS, relative_path) + return os.path.join(os.path.abspath("."), relative_path) + +def terminate_process(process_name): + try: + print(f"[INFO] Attempting to terminate process: {process_name}") + subprocess.run(f"taskkill /F /IM {process_name}", shell=True, check=True) + print(f"[INFO] Successfully terminated process: {process_name}") + return 0 + except subprocess.CalledProcessError as e: + if "128" in str(e): print(f"[WARN] Process '{process_name}' not found or already terminated.") + else: print(f"[ERROR] Failed to terminate process '{process_name}'. Error: {e}") + except Exception as e: + print(f"[ERROR] Unexpected error while terminating '{process_name}'. Error: {e}") + return 1 + +def start_process(process_name): + try: + print(f"[INFO] Attempting to start process: {process_name}") + subprocess.run(f"start {process_name}", shell=True, check=True) + print(f"[INFO] Successfully started process: {process_name}") + return 0 + except FileNotFoundError: + print(f"[ERROR] Process '{process_name}' not found. Please check the path or name.") + except subprocess.CalledProcessError as e: + print(f"[ERROR] Failed to start process '{process_name}'. Error: {e}") + except Exception as e: + print(f"[ERROR] Unexpected error while starting '{process_name}'. Error: {e}") + return 1 + +""" +Pyinstaller splash screen related +""" +def check_alpha(img): + """Check if the image contains translucent pixels.""" + alpha_channel = img.getchannel('A') + alpha_data = alpha_channel.load() + for y in range(img.height): + for x in range(img.width): + alpha_value = alpha_data[x, y] + if 0 < alpha_value < 255: # Check if translucent + return True + return False + +def processing_image(img_path): + img = Image.open(img_path).convert("RGBA") # Ensure image is in RGBA format + print("Contain translucency pixels (Before):", check_alpha(img)) + + # Remove translucency: Set all non-opaque pixels (alpha != 255) to transparent (alpha = 0) + pixels = img.load() + for y in range(img.height): + for x in range(img.width): + r, g, b, a = pixels[x, y] + if a != 255: # If alpha is not fully opaque + pixels[x, y] = (r, g, b, 0) # Make pixel fully transparent + + print("Contain translucency pixels (After):", check_alpha(img)) + + # Save the modified image + output_path = img_path.replace(".png", "_transparent.png") + img.save(output_path, "PNG") + print(f"Processed image saved to {output_path}") + return output_path \ No newline at end of file diff --git a/widget.py b/widget.py new file mode 100644 index 0000000..f25a9a8 --- /dev/null +++ b/widget.py @@ -0,0 +1,126 @@ +import sys +from utils.utils import resource_path +from PyQt5.QtWidgets import QApplication, QWidget, QRubberBand, QMessageBox +from PyQt5.QtCore import Qt, QRect, QPoint, QSize +from PyQt5.QtGui import QGuiApplication, QCursor + + +class SnippingWidget(QWidget): + def __init__(self, mainwindow): + super().__init__() + self.setWindowTitle("Screen Snipper") + self.parent = mainwindow + self.setWindowIcon(self.parent.icon) + self.rectMode = True + + # ✅ 최상단 유지 및 입력 포커스 확보 + self.setWindowFlags( + Qt.FramelessWindowHint + | Qt.WindowStaysOnTopHint + | Qt.SplashScreen # ✅ 최상단 유지 및 입력 포커스 확보 + ) + + self.setWindowOpacity(0.3) # ✅ 창을 불투명하게 유지 + self.setAttribute(Qt.WA_NoSystemBackground, False) # ✅ 배경 활성화 + self.clipboard = QApplication.clipboard() + + # ✅ 전체 화면 설정 + screen_geometry = QGuiApplication.primaryScreen().geometry() + self.setGeometry(screen_geometry) + self.activateWindow() + self.raise_() + + # ✅ 드래그 초기화 + self.origin = QPoint() + self.rubberBand = QRubberBand(QRubberBand.Rectangle, self) + + def showEvent(self, event): + self.setCursor(Qt.CrossCursor) # ✅ 십자 모양 커서로 변경 + super().showEvent(event) + + def closeEvent(self, event): + self.unsetCursor() + super().closeEvent(event) + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton: + if self.rectMode == True: + self.origin = event.pos() + self.rubberBand.setGeometry(QRect(self.origin, QSize())) + self.rubberBand.show() + event.accept() + + def mouseMoveEvent(self, event): + if self.rectMode == True: + if not self.origin.isNull(): + rect = QRect(self.origin, event.pos()).normalized() + self.rubberBand.setGeometry(rect) + event.accept() + + def mouseReleaseEvent(self, event): + if event.button() == Qt.LeftButton: + + """ Button Coordinate Setting Mode """ + if self.rectMode == False: + pos = event.pos() + btn_x, btn_y = pos.x(), pos.y() + self.parent.coords["btn_x"] = btn_x + self.parent.coords["btn_y"] = btn_y + self.parent.update_btn_coord() + + self.close_overlay() + + QMessageBox.information( + self, + "버튼 좌표 지정 완료", + f"x: {btn_x}, y: {btn_y}", + ) + + """ Snipping Area Setting Mode """ + if self.rectMode == True: + selected_rect = self.rubberBand.geometry() + + self.rubberBand.hide() + QApplication.processEvents() + self.close_overlay() + + # 멀티 모니터 지원 + screen = QGuiApplication.screenAt(event.globalPos()) + if not screen: + screen = QGuiApplication.primaryScreen() + + # screenshot = screen.grabWindow( + # 0, + # selected_rect.x(), + # selected_rect.y(), + # selected_rect.width(), + # selected_rect.height(), + # ) + + # # ✅ 캡처 이미지 저장 및 클립보드 복사 + # screenshot.save("screenshot.png", "PNG") + # self.clipboard.setPixmap(screenshot) + + x, y, w, h = selected_rect.getRect() + self.parent.coords["x"] = x + self.parent.coords["y"] = y + self.parent.coords["w"] = w + self.parent.coords["h"] = h + + self.parent.update_rect_coords() + + QMessageBox.information( + self, + "영역 지정 완료", + f"x: {x}, y: {y}, width: {w}, height: {h}", + ) + def close_overlay(self): + self.origin = QPoint() + self.close() + + def keyPressEvent(self, event): + if event.key() == Qt.Key_Escape: + self.rubberBand.hide() + self.origin = QPoint() + self.close() + event.accept()