Files
Android-pyApi/android_pyapi/android/graphics_pdf.py
T
shuiyu1123 bfe27fdc08 feat: Android-pyApi v1.0 - 全面翻译 Android SDK API 为 Python
52 个模块覆盖 Android + Java + org.json 标准库
2026-06-24 11:16:16 +00:00

335 lines
12 KiB
Python

"""
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",
]