""" Android Graphics PDF 组件 — PdfDocument, PdfRenderer, ParcelFileDescriptor 方法签名完全对齐 Android SDK """ import os import io import tempfile from typing import Any, Dict, List, Optional, Tuple from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL, HAS_PYMUPDF, HAS_PDF2IMAGE, resolve_android_path # ═══════════════════════════════════════════════════════════════ # ParcelFileDescriptor # ═══════════════════════════════════════════════════════════════ class ParcelFileDescriptor(Stub): """ParcelFileDescriptor — 文件描述符""" MODE_READ_ONLY = 0x10000000 MODE_WRITE_ONLY = 0x20000000 MODE_READ_WRITE = 0x30000000 MODE_CREATE = 0x08000000 MODE_TRUNCATE = 0x04000000 MODE_APPEND = 0x02000000 def __init__(self, fd: int = -1, file_path: str = ""): super().__init__() self._fd = fd self._file_path = file_path self._closed = False self._file_obj = None if file_path and fd < 0: resolved = resolve_android_path(file_path) self._file_path = resolved @staticmethod def open(file_path: str, mode: int) -> "ParcelFileDescriptor": resolved = resolve_android_path(file_path) fmode = "rb" if mode & ParcelFileDescriptor.MODE_WRITE_ONLY: fmode = "wb" elif mode & ParcelFileDescriptor.MODE_READ_WRITE: fmode = "r+b" if mode & ParcelFileDescriptor.MODE_CREATE: if not os.path.exists(os.path.dirname(resolved)): os.makedirs(os.path.dirname(resolved), exist_ok=True) if not os.path.exists(resolved): open(resolved, "w").close() return ParcelFileDescriptor(-1, file_path) @staticmethod def parseFromFd(fd: int) -> "ParcelFileDescriptor": return ParcelFileDescriptor(fd) @staticmethod def parseFromString(path: str) -> "ParcelFileDescriptor": resolved = resolve_android_path(path) return ParcelFileDescriptor(-1, resolved) @staticmethod def adoptFd(fd: int) -> "ParcelFileDescriptor": return ParcelFileDescriptor(fd) @staticmethod def createPipe() -> List["ParcelFileDescriptor"]: r, w = os.pipe() return [ParcelFileDescriptor(r), ParcelFileDescriptor(w)] def getFileDescriptor(self) -> int: return self._fd def getStatSize(self) -> int: if self._file_path and os.path.exists(self._file_path): return os.path.getsize(self._file_path) return 0 def getFd(self) -> int: return self._fd def close(self): if not self._closed: self._closed = True if self._file_obj: self._file_obj.close() elif self._fd >= 0: try: os.close(self._fd) except OSError: pass def getFilePath(self) -> str: return self._file_path def toString(self) -> str: return f"ParcelFileDescriptor(fd={self._fd})" # ═══════════════════════════════════════════════════════════════ # PdfDocument # ═══════════════════════════════════════════════════════════════ class PdfDocument(Stub): """PdfDocument — PDF 文档""" class Page(Stub): """PdfDocument.Page — PDF 页面""" def __init__(self, page_number: int, width: int, height: int): super().__init__() self._page_number = page_number self._width = width self._height = height self._canvas = None def getCanvas(self) -> "Canvas": from .graphics import Canvas if self._canvas is None: self._canvas = Canvas(self._width, self._height) return self._canvas def getWidth(self) -> int: return self._width def getHeight(self) -> int: return self._height def start(self): pass def finish(self): pass def __init__(self): super().__init__() self._pages: List[PdfDocument.Page] = [] def startPage(self, page_size: "Page") -> "Canvas": width = page_size.getWidth() if hasattr(page_size, "getWidth") else 595 height = page_size.getHeight() if hasattr(page_size, "getHeight") else 842 page = PdfDocument.Page(len(self._pages), width, height) self._pages.append(page) return page.getCanvas() def finishPage(self, page: "Page"): page.finish() def getPages(self) -> List["Page"]: return list(self._pages) def writeTo(self, out_stream, close: bool = True): if HAS_PYMUPDF: import fitz doc = fitz.open() for p in self._pages: page = doc.new_page(width=p._width, height=p._height) if HAS_PIL and hasattr(p._canvas, "_image") and p._canvas._image: img_bytes = io.BytesIO() p._canvas._image.save(img_bytes, format="PNG") img_bytes.seek(0) rect = page.rect page.insert_image(rect, stream=img_bytes.read()) doc.save(out_stream) doc.close() elif HAS_PIL: from PIL import Image images = [] for p in self._pages: if hasattr(p._canvas, "_image") and p._canvas._image: images.append(p._canvas._image.convert("RGB")) if images: images[0].save(out_stream, format="PDF", save_all=True, append_images=images[1:]) else: blank = Image.new("RGB", (595, 842), (255, 255, 255)) blank.save(out_stream, format="PDF") else: if close: out_stream.close() def close(self): self._pages.clear() def __enter__(self): return self def __exit__(self, *args): self.close() # ═══════════════════════════════════════════════════════════════ # PdfRenderer # ═══════════════════════════════════════════════════════════════ class PdfRenderer(Stub): """PdfRenderer — PDF 渲染器""" RENDER_MODE_FOR_DISPLAY = 1 RENDER_MODE_FOR_PRINT = 2 RENDER_MODE_FOR_ALL = 3 class Page(Stub): """PdfRenderer.Page — PDF 渲染页面""" def __init__(self, renderer: "PdfRenderer", index: int): super().__init__() self._renderer = renderer self._index = index self._opened = True if renderer._doc: self._fitz_page = renderer._doc[index] rect = self._fitz_page.rect self._width = int(rect.width) self._height = int(rect.height) else: self._fitz_page = None self._width = 595 self._height = 842 def getWidth(self) -> int: return self._width def getHeight(self) -> int: return self._height def getIndex(self) -> int: return self._index def render(self, canvas: "Canvas", destLeft: int, destTop: int, destRight: int, destBottom: int, flags: int = 0): if not self._opened: return if HAS_PYMUPDF and self._fitz_page: from .graphics import Bitmap mat = fitz.Matrix(2, 2) # 2x zoom for quality pix = self._fitz_page.get_pixmap(matrix=mat) img_data = pix.tobytes("png") if HAS_PIL and hasattr(canvas, "_image") and canvas._image: from PIL import Image import io as _io img = Image.open(io.BytesIO(img_data)) img = img.resize((destRight - destLeft, destBottom - destTop)) canvas._image.paste(img, (destLeft, destTop)) elif HAS_PDF2IMAGE and HAS_PIL and hasattr(canvas, "_image"): from pdf2image import convert_from_bytes if self._renderer._pdf_bytes: images = convert_from_bytes( self._renderer._pdf_bytes, first_page=self._index + 1, last_page=self._index + 1 ) if images: img = images[0].resize((destRight - destLeft, destBottom - destTop)) canvas._image.paste(img.convert("RGBA"), (destLeft, destTop)) def start(self): self._opened = True def finish(self): self._opened = False def close(self): self._opened = False def __init__(self, pfd: ParcelFileDescriptor, closePfd: bool = True): super().__init__() self._pfd = pfd self._close_pfd = closePfd self._page_count = 0 self._pdf_bytes = None self._doc = None file_path = pfd.getFilePath() if file_path and os.path.exists(file_path): with open(file_path, "rb") as f: self._pdf_bytes = f.read() elif pfd._fd >= 0: try: self._pdf_bytes = os.read(pfd._fd, 10 * 1024 * 1024) except OSError: self._pdf_bytes = None if HAS_PYMUPDF and self._pdf_bytes: import fitz self._doc = fitz.open(stream=self._pdf_bytes, filetype="pdf") self._page_count = len(self._doc) elif self._pdf_bytes: self._page_count = 1 # fallback else: self._page_count = 0 def openPage(self, index: int) -> "Page": if 0 <= index < self._page_count: return PdfRenderer.Page(self, index) raise IndexError(f"Page index {index} out of range (0-{self._page_count - 1})") def close(self): if self._doc and HAS_PYMUPDF: self._doc.close() self._doc = None if self._close_pfd and self._pfd: self._pfd.close() self._pdf_bytes = None def getPageCount(self) -> int: return self._page_count def shouldScale(self) -> bool: return False def __enter__(self): return self def __exit__(self, *args): self.close() # ═══════════════════════════════════════════════════════════════ # PdfDocumentPage (alias for PdfDocument.Page) # ═══════════════════════════════════════════════════════════════ PdfDocumentPage = PdfDocument.Page # ═══════════════════════════════════════════════════════════════ # __all__ # ═══════════════════════════════════════════════════════════════ __all__ = [ "PdfDocument", "PdfDocumentPage", "PdfRenderer", "ParcelFileDescriptor", ]