Published on

파이썬으로 클립보드 이미지를 파일로 저장하는 방법 윈도우 맥 리눅스까지

Authors

서류에 붙여 넣으려고 캡처한 이미지가 어디 저장됐는지 찾다가 시간을 날린 적이 있나요? 혹은 디자이너가 슬랙에 던져준 스크린샷을 하나씩 다운로드하기 귀찮아서, 그냥 복사(Ctrl+C)한 걸 바로 파일로 떨어뜨리고 싶었던 적도 있을 겁니다.

파이썬으로 클립보드(Clipboard)에 들어 있는 이미지를 읽어 PNG/JPG 파일로 저장하면 이런 반복 작업을 자동화할 수 있습니다. 문제는 운영체제마다 클립보드 API가 다르고, “이미지처럼 보이는데 실제로는 파일 경로/HTML/바이너리”로 들어오는 경우가 많아 트러블슈팅 포인트가 꽤 있다는 점입니다.

이 글에서는 다음을 목표로 합니다.

  • 윈도우/맥/리눅스에서 클립보드 이미지를 저장하는 대표 방법
  • 실패하는 케이스(이미지가 아닌 데이터, 권한/의존성 문제) 해결
  • 실무에서 바로 쓰는 “자동 파일명 + 저장 폴더 + 포맷 변환” 스크립트

파이썬 기초 문법이 필요하다면: 파이썬 문법 핵심 정리 초보 코딩 학습 가이드 10가지


클립보드 이미지 저장의 핵심 개념

클립보드에는 단일 데이터만 들어가는 게 아니라, 같은 콘텐츠가 여러 포맷으로 동시에 들어갈 수 있습니다.

  • 이미지 자체 (bitmap/PNG 등)
  • 파일 드래그로 복사한 경우: 파일 경로 리스트
  • 브라우저에서 복사: HTML 조각 + 이미지 URL + (가끔) 실제 이미지

따라서 “클립보드에 뭐가 들어 있지?”를 먼저 판단하고, 그에 맞는 로직을 타야 안정적입니다.


가장 쉬운 방법 윈도우와 맥에서 PIL ImageGrab 사용

Pillow(PIL)의 ImageGrab.grabclipboard()윈도우/맥에서 클립보드 이미지를 비교적 간단히 가져올 수 있습니다.

설치

pip install pillow

기본 저장 스크립트

from PIL import ImageGrab
from pathlib import Path
from datetime import datetime


def save_clipboard_image(out_dir: str = "./clips", fmt: str = "PNG") -> Path:
    out_path = Path(out_dir)
    out_path.mkdir(parents=True, exist_ok=True)

    data = ImageGrab.grabclipboard()

    # data는 보통 다음 중 하나
    # - PIL.Image.Image (이미지)
    # - list[str] (파일 경로들)
    # - None

    if data is None:
        raise RuntimeError("클립보드에 이미지가 없습니다.")

    if isinstance(data, list):
        raise RuntimeError(f"클립보드에는 이미지가 아니라 파일 경로가 들어 있습니다: {data[:3]}")

    if not hasattr(data, "save"):
        raise RuntimeError(f"알 수 없는 클립보드 데이터 타입: {type(data)}")

    ts = datetime.now().strftime("%Y%m%d-%H%M%S")
    file_path = out_path / f"clipboard-{ts}.{fmt.lower()}"

    # PNG는 무손실이라 기본으로 추천
    data.save(file_path, format=fmt)
    return file_path


if __name__ == "__main__":
    saved = save_clipboard_image(fmt="PNG")
    print("saved:", saved)

자주 만나는 실패 케이스와 대응

  • 캡처 툴에 따라 클립보드에 이미지가 아닌 ‘파일’이 들어감

    • 예: 캡처 후 자동으로 파일 생성 → 파일 경로가 복사됨
    • 위 코드에서 list로 감지되면 안내 메시지로 처리
  • 브라우저에서 이미지 복사했는데 None이 나옴

    • 브라우저가 실제 이미지 비트를 클립보드에 안 넣고 HTML/URL만 넣는 경우
    • 해결: “이미지 복사” 대신 “이미지 저장 후 파일 복사” 또는 OS별 다른 접근(아래 섹션)

윈도우에서 더 강력하게 처리하기 pywin32로 DIB 읽기

ImageGrab이 실패하거나, 특정 앱(원격 데스크톱/보안툴)에서 호환이 애매할 때는 Windows Clipboard API로 **CF_DIB(Device Independent Bitmap)**를 직접 읽는 방식이 더 단단합니다.

설치

pip install pywin32 pillow

CF_DIB를 PNG로 저장

import win32clipboard
from PIL import Image
from io import BytesIO
from pathlib import Path
from datetime import datetime


def save_windows_clipboard_dib(out_dir="./clips"):
    Path(out_dir).mkdir(parents=True, exist_ok=True)

    win32clipboard.OpenClipboard()
    try:
        if not win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_DIB):
            raise RuntimeError("클립보드에 DIB 이미지 포맷이 없습니다.")

        dib = win32clipboard.GetClipboardData(win32clipboard.CF_DIB)
    finally:
        win32clipboard.CloseClipboard()

    # DIB는 BMP 헤더가 빠진 경우가 많아, BMP 파일 헤더를 붙여 PIL로 읽습니다.
    bmp_header = (
        b"BM" +
        (len(dib) + 14).to_bytes(4, "little") +
        b"\x00\x00\x00\x00" +
        (14 + 40).to_bytes(4, "little")
    )

    bmp_bytes = bmp_header + dib
    img = Image.open(BytesIO(bmp_bytes))

    ts = datetime.now().strftime("%Y%m%d-%H%M%S")
    out = Path(out_dir) / f"clipboard-{ts}.png"
    img.save(out, "PNG")
    return out


if __name__ == "__main__":
    print(save_windows_clipboard_dib())

트러블슈팅

  • OpenClipboard가 실패한다면
    • 다른 프로그램이 클립보드를 잠그고 있는 경우가 있습니다.
    • 해결: 잠깐 기다렸다 재시도(백오프) 로직을 넣는 게 실무적으로 좋습니다.

리눅스에서 처리하기 xclip wl-clipboard와의 현실적인 타협

리눅스는 배포판/디스플레이 서버(X11 vs Wayland)에 따라 클립보드 접근 방식이 갈립니다.

  • X11: xclip, xsel
  • Wayland: wl-clipboard (wl-paste)

즉, 파이썬만으로 “어디서나”를 만들기 어렵고, 보통 외부 커맨드를 호출해서 이미지 바이너리를 받아 저장하는 패턴이 가장 실용적입니다.

리눅스 환경 자체가 익숙하지 않다면: 리눅스란 무엇인가? 자유로운 운영체제의 세계

X11 xclip로 PNG 저장

# Ubuntu 예시
sudo apt-get install -y xclip
import subprocess
from pathlib import Path
from datetime import datetime


def save_linux_clipboard_image_x11(out_dir="./clips"):
    Path(out_dir).mkdir(parents=True, exist_ok=True)

    ts = datetime.now().strftime("%Y%m%d-%H%M%S")
    out = Path(out_dir) / f"clipboard-{ts}.png"

    # image/png로 요청
    p = subprocess.run(
        ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=False,
    )

    if p.returncode != 0 or not p.stdout:
        raise RuntimeError(
            "클립보드에서 image/png를 읽지 못했습니다. "
            "Wayland인지, 클립보드에 이미지가 없는지 확인하세요.\n"
            f"stderr: {p.stderr.decode(errors='ignore')}"
        )

    out.write_bytes(p.stdout)
    return out


if __name__ == "__main__":
    print(save_linux_clipboard_image_x11())

Wayland wl-clipboard로 PNG 저장

sudo apt-get install -y wl-clipboard
import subprocess
from pathlib import Path
from datetime import datetime


def save_linux_clipboard_image_wayland(out_dir="./clips"):
    Path(out_dir).mkdir(parents=True, exist_ok=True)

    ts = datetime.now().strftime("%Y%m%d-%H%M%S")
    out = Path(out_dir) / f"clipboard-{ts}.png"

    p = subprocess.run(
        ["wl-paste", "--type", "image/png"],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=False,
    )

    if p.returncode != 0 or not p.stdout:
        raise RuntimeError(
            "클립보드에서 image/png를 읽지 못했습니다.\n"
            f"stderr: {p.stderr.decode(errors='ignore')}"
        )

    out.write_bytes(p.stdout)
    return out


if __name__ == "__main__":
    print(save_linux_clipboard_image_wayland())

리눅스 트러블슈팅 체크리스트

  • X11인데 xclip -t image/png -o가 빈 출력

    • 클립보드에 이미지가 아니라 텍스트/HTML만 있는 경우
    • 다른 MIME 타입(image/bmp, image/jpeg)이 있는지 확인 필요
  • Wayland에서 xclip이 동작하지 않음

    • 정상입니다. Wayland에서는 wl-clipboard를 쓰는 쪽이 정석입니다.

운영체제 자동 감지해서 하나의 스크립트로 만들기

실무에서는 “내 PC에서는 되는데 팀원 PC에서는 안 됨”이 가장 큰 비용입니다. 아래는 OS를 감지해 가능한 전략을 순서대로 시도하는 형태입니다.

import os
import platform
import subprocess
from pathlib import Path
from datetime import datetime

from PIL import ImageGrab


def _save_bytes(out_dir, ext, data: bytes) -> Path:
    Path(out_dir).mkdir(parents=True, exist_ok=True)
    ts = datetime.now().strftime("%Y%m%d-%H%M%S")
    out = Path(out_dir) / f"clipboard-{ts}.{ext}"
    out.write_bytes(data)
    return out


def save_clipboard_image_cross_platform(out_dir="./clips") -> Path:
    system = platform.system().lower()

    # 1) Windows/Mac: Pillow ImageGrab 우선
    if system in ("windows", "darwin"):
        data = ImageGrab.grabclipboard()
        if data is None:
            raise RuntimeError("클립보드에 이미지가 없습니다.")
        if isinstance(data, list):
            raise RuntimeError(f"클립보드에는 파일 경로가 들어 있습니다: {data[:3]}")
        ts = datetime.now().strftime("%Y%m%d-%H%M%S")
        Path(out_dir).mkdir(parents=True, exist_ok=True)
        out = Path(out_dir) / f"clipboard-{ts}.png"
        data.save(out, "PNG")
        return out

    # 2) Linux: Wayland 우선 시도, 실패하면 X11
    if system == "linux":
        # Wayland 여부 힌트
        is_wayland = bool(os.environ.get("WAYLAND_DISPLAY"))

        if is_wayland:
            p = subprocess.run(["wl-paste", "--type", "image/png"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            if p.returncode == 0 and p.stdout:
                return _save_bytes(out_dir, "png", p.stdout)

        p = subprocess.run(["xclip", "-selection", "clipboard", "-t", "image/png", "-o"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        if p.returncode == 0 and p.stdout:
            return _save_bytes(out_dir, "png", p.stdout)

        raise RuntimeError("리눅스에서 클립보드 image/png를 가져오지 못했습니다. wl-clipboard 또는 xclip 설치/환경을 확인하세요.")

    raise RuntimeError(f"지원하지 않는 OS: {system}")


if __name__ == "__main__":
    print(save_clipboard_image_cross_platform())

Best Practice

  • 저장 포맷은 기본 PNG
    • 문서/스크린샷은 PNG가 품질 대비 용량이 합리적이고, 재인코딩 손실이 없습니다.
  • 파일명에 타임스탬프
    • 덮어쓰기 방지 + 정렬이 쉽습니다.
  • 실패 메시지를 친절하게
    • “이미지 없음”과 “파일 경로가 들어 있음”은 완전히 다른 문제입니다.

응용 자동으로 클립보드 감시해서 저장하기

“복사할 때마다 자동 저장”을 원하면 폴링(polling) 방식이 간단합니다. 완전한 이벤트 기반은 OS별 구현이 달라 복잡해지기 쉽습니다.

아래는 윈도우/맥에서 ImageGrab이 반환하는 객체가 바뀌는지 대략 감지하는 예시입니다(완벽하진 않지만 실용적).

import time
from PIL import ImageGrab


def watch_and_save(interval=0.8):
    last_sig = None

    while True:
        data = ImageGrab.grabclipboard()
        if hasattr(data, "size"):
            sig = (data.size, getattr(data, "mode", None))
            if sig != last_sig:
                last_sig = sig
                # 여기서 앞서 만든 save 함수 호출
                from pathlib import Path
                from datetime import datetime
                Path("./clips").mkdir(exist_ok=True)
                ts = datetime.now().strftime("%Y%m%d-%H%M%S")
                out = Path("./clips") / f"clipboard-{ts}.png"
                data.save(out, "PNG")
                print("saved", out)

        time.sleep(interval)


if __name__ == "__main__":
    watch_and_save()

폴링 기반 감시는 CPU를 거의 쓰지 않지만, 다음 한계가 있습니다.

  • 같은 크기/모드의 다른 이미지면 변경 감지가 약할 수 있음 → 픽셀 해시를 계산하면 개선되지만 비용 증가
  • 리눅스/Wayland는 별도 커맨드 기반으로 감시 로직을 새로 짜야 함

결론

  • 윈도우/맥은 Pillow(ImageGrab.grabclipboard)가장 빠르게 클립보드 이미지를 파일로 저장할 수 있습니다.
  • 윈도우에서 호환성이 필요하면 pywin32CF_DIB를 직접 읽는 방식이 더 강력합니다.
  • 리눅스는 X11/Wayland 차이 때문에 xclip/wl-clipboard 같은 외부 도구 연동이 현실적인 해법입니다.
  • 실무에서는 “클립보드에 이미지가 아닌 데이터가 들어 있는 경우”를 반드시 분기 처리하고, 친절한 에러/가이드를 제공하는 것이 유지보수 비용을 크게 줄입니다.