""" Android Graphics 组件 — Bitmap, Canvas, Paint, Color, Path, Rect 等 方法签名完全对齐 Android SDK """ import array import math import struct import io import os from enum import IntEnum from typing import Any, Dict, List, Optional, Sequence, Tuple, Union from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL, resolve_android_path # Re-export Typeface from text module from .text import Typeface # ═══════════════════════════════════════════════════════════════ # Color # ═══════════════════════════════════════════════════════════════ class Color(Stub): """Color — 颜色工具类""" BLACK = 0xFF000000 DKGRAY = 0xFF444444 GRAY = 0xFF888888 LTGRAY = 0xFFCCCCCC WHITE = 0xFFFFFFFF RED = 0xFFFF0000 GREEN = 0xFF00FF00 BLUE = 0xFF0000FF YELLOW = 0xFFFFFF00 CYAN = 0xFF00FFFF MAGENTA = 0xFFFF00FF TRANSPARENT = 0 @staticmethod def alpha(color: int) -> int: return (color >> 24) & 0xFF @staticmethod def red(color: int) -> int: return (color >> 16) & 0xFF @staticmethod def green(color: int) -> int: return (color >> 8) & 0xFF @staticmethod def blue(color: int) -> int: return color & 0xFF @staticmethod def rgb(r: int, g: int, b: int) -> int: return 0xFF000000 | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF) @staticmethod def argb(a: int, r: int, g: int, b: int) -> int: return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF) @staticmethod def parseColor(colorString: str) -> int: s = colorString.strip() if s.startswith("#"): s = s[1:] if len(s) == 3: s = "".join(c * 2 for c in s) if len(s) == 6: s = "FF" + s return int(s, 16) & 0xFFFFFFFF if s.startswith("0x"): return int(s, 16) & 0xFFFFFFFF name_map = { "black": Color.BLACK, "white": Color.WHITE, "red": Color.RED, "green": Color.GREEN, "blue": Color.BLUE, "yellow": Color.YELLOW, "cyan": Color.CYAN, "magenta": Color.MAGENTA, "transparent": Color.TRANSPARENT, "dkgray": Color.DKGRAY, "gray": Color.GRAY, "ltgray": Color.LTGRAY, "darkgray": Color.DKGRAY, "lightgray": Color.LTGRAY, } return name_map.get(s.lower(), Color.BLACK) @staticmethod def RGBToHSV(red: int, green: int, blue: int, hsv: List[float]): r, g, b = red / 255.0, green / 255.0, blue / 255.0 mx = max(r, g, b) mn = min(r, g, b) df = mx - mn if df == 0: h = 0.0 elif mx == r: h = ((g - b) / df) % 6.0 elif mx == g: h = (b - r) / df + 2.0 else: h = (r - g) / df + 4.0 h = h * 60.0 if h < 0: h += 360.0 s = 0.0 if mx == 0 else (df / mx) v = mx hsv[0] = h hsv[1] = s hsv[2] = v @staticmethod def HSVToColor(hsv: List[float]) -> int: return Color.HSVToColor(255, hsv) @staticmethod def HSVToColor(alpha: int, hsv: List[float]) -> int: h, s, v = hsv[0], hsv[1], hsv[2] hi = int(h / 60.0) % 6 f = (h / 60.0) - hi p = v * (1.0 - s) q = v * (1.0 - f * s) t = v * (1.0 - (1.0 - f) * s) if hi == 0: r, g, b = v, t, p elif hi == 1: r, g, b = q, v, p elif hi == 2: r, g, b = p, v, t elif hi == 3: r, g, b = p, q, v elif hi == 4: r, g, b = t, p, v else: r, g, b = v, p, q return Color.argb(alpha, int(r * 255 + 0.5), int(g * 255 + 0.5), int(b * 255 + 0.5)) @staticmethod def ColorToHSV(color: int, hsv: List[float]): Color.RGBToHSV(Color.red(color), Color.green(color), Color.blue(color), hsv) @staticmethod def getHue(color: int) -> float: hsv = [0.0, 0.0, 0.0] Color.ColorToHSV(color, hsv) return hsv[0] @staticmethod def getSaturation(color: int) -> float: hsv = [0.0, 0.0, 0.0] Color.ColorToHSV(color, hsv) return hsv[1] @staticmethod def getBrightness(color: int) -> float: hsv = [0.0, 0.0, 0.0] Color.ColorToHSV(color, hsv) return hsv[2] @staticmethod def HSLToColor(hsl: List[float], alpha: float = 1.0) -> int: h, s, l = hsl[0] / 360.0, hsl[1], hsl[2] if s == 0: r = g = b = l else: def hue2rgb(p, q, t): if t < 0: t += 1 if t > 1: t -= 1 if t < 1.0 / 6.0: return p + (q - p) * 6.0 * t if t < 1.0 / 2.0: return q if t < 2.0 / 3.0: return p + (q - p) * (2.0 / 3.0 - t) * 6.0 return p q = l * (1 + s) if l < 0.5 else l + s - l * s p = 2 * l - q r = hue2rgb(p, q, h + 1.0 / 3.0) g = hue2rgb(p, q, h) b = hue2rgb(p, q, h - 1.0 / 3.0) a = max(0, min(255, int(alpha * 255))) return Color.argb(a, int(r * 255), int(g * 255), int(b * 255)) @staticmethod def ColorToHSL(color: int, hsl: List[float]): r, g, b = Color.red(color) / 255.0, Color.green(color) / 255.0, Color.blue(color) / 255.0 mx = max(r, g, b) mn = min(r, g, b) l = (mx + mn) / 2.0 if mx == mn: h = 0.0 s = 0.0 else: d = mx - mn s = d / (1 - abs(2 * l - 1)) if mx == r: h = ((g - b) / d) % 6.0 elif mx == g: h = (b - r) / d + 2.0 else: h = (r - g) / d + 4.0 h = h * 60.0 if h < 0: h += 360.0 hsl[0] = h hsl[1] = s hsl[2] = l # ═══════════════════════════════════════════════════════════════ # Rect # ═══════════════════════════════════════════════════════════════ class Rect(Stub): """Rect — 矩形 (int 坐标)""" def __init__(self, left: int = 0, top: int = 0, right: int = 0, bottom: int = 0): super().__init__() self.left = left self.top = top self.right = right self.bottom = bottom def width(self) -> int: return self.right - self.left def height(self) -> int: return self.bottom - self.top def centerX(self) -> int: return (self.left + self.right) // 2 def centerY(self) -> int: return (self.top + self.bottom) // 2 def exactCenterX(self) -> float: return (self.left + self.right) * 0.5 def exactCenterY(self) -> float: return (self.top + self.bottom) * 0.5 def set(self, left: int, top: int, right: int, bottom: int): self.left, self.top, self.right, self.bottom = left, top, right, bottom def offset(self, dx: int, dy: int): self.left += dx self.top += dy self.right += dx self.bottom += dy def offsetTo(self, newLeft: int, newTop: int): self.right += newLeft - self.left self.bottom += newTop - self.top self.left = newLeft self.top = newTop def inset(self, dx: int, dy: int): self.left += dx self.top += dy self.right -= dx self.bottom -= dy def contains(self, *args) -> bool: if len(args) == 2: x, y = args return (self.left < self.right and self.top < self.bottom and x >= self.left and x < self.right and y >= self.top and y < self.bottom) elif len(args) == 4: left, top, right, bottom = args return (self.left < self.right and self.top < self.bottom and self.left <= left and self.top <= top and self.right >= right and self.bottom >= bottom) return False def intersect(self, left: int, top: int, right: int, bottom: int) -> bool: nL = max(self.left, left) nT = max(self.top, top) nR = min(self.right, right) nB = min(self.bottom, bottom) if nL < nR and nT < nB: self.left, self.top, self.right, self.bottom = nL, nT, nR, nB return True return False def intersects(self, left: int, top: int, right: int, bottom: int) -> bool: return self.left < right and self.right > left \ and self.top < bottom and self.bottom > top @staticmethod def intersects(a: "Rect", b: "Rect") -> bool: return a.left < b.right and a.right > b.left \ and a.top < b.bottom and a.bottom > b.top def union(self, left: int, top: int, right: int, bottom: int): if self.isEmpty(): self.set(left, top, right, bottom) else: self.left = min(self.left, left) self.top = min(self.top, top) self.right = max(self.right, right) self.bottom = max(self.bottom, bottom) def sort(self): if self.left > self.right: self.left, self.right = self.right, self.left if self.top > self.bottom: self.top, self.bottom = self.bottom, self.top def isEmpty(self) -> bool: return self.left >= self.right or self.top >= self.bottom def setEmpty(self): self.left = self.top = self.right = self.bottom = 0 def toShortString(self) -> str: return f"[{self.left},{self.top}][{self.right},{self.bottom}]" def flattenToString(self) -> str: return f"{self.left} {self.top} {self.right} {self.bottom}" @staticmethod def unflattenFromString(s: str) -> "Rect": parts = s.split() if len(parts) >= 4: return Rect(int(parts[0]), int(parts[1]), int(parts[2]), int(parts[3])) return Rect() def toString(self) -> str: return f"Rect({self.left}, {self.top}, {self.right}, {self.bottom})" # ═══════════════════════════════════════════════════════════════ # RectF # ═══════════════════════════════════════════════════════════════ class RectF(Stub): """RectF — 矩形 (float 坐标)""" def __init__(self, left: float = 0.0, top: float = 0.0, right: float = 0.0, bottom: float = 0.0): super().__init__() self.left = left self.top = top self.right = right self.bottom = bottom def width(self) -> float: return self.right - self.left def height(self) -> float: return self.bottom - self.top def centerX(self) -> float: return (self.left + self.right) * 0.5 def centerY(self) -> float: return (self.top + self.bottom) * 0.5 def set(self, left: float, top: float, right: float, bottom: float): self.left, self.top, self.right, self.bottom = left, top, right, bottom def offset(self, dx: float, dy: float): self.left += dx self.top += dy self.right += dx self.bottom += dy def offsetTo(self, newLeft: float, newTop: float): self.right += newLeft - self.left self.bottom += newTop - self.top self.left = newLeft self.top = newTop def inset(self, dx: float, dy: float): self.left += dx self.top += dy self.right -= dx self.bottom -= dy def contains(self, x: float, y: float) -> bool: return self.left < self.right and self.top < self.bottom \ and x >= self.left and x < self.right and y >= self.top and y < self.bottom def intersect(self, left: float, top: float, right: float, bottom: float) -> bool: nL = max(self.left, left) nT = max(self.top, top) nR = min(self.right, right) nB = min(self.bottom, bottom) if nL < nR and nT < nB: self.left, self.top, self.right, self.bottom = nL, nT, nR, nB return True return False def intersects(self, left: float, top: float, right: float, bottom: float) -> bool: return self.left < right and self.right > left \ and self.top < bottom and self.bottom > top @staticmethod def intersects(a: "RectF", b: "RectF") -> bool: return a.left < b.right and a.right > b.left \ and a.top < b.bottom and a.bottom > b.top def union(self, left: float, top: float, right: float, bottom: float): if self.isEmpty(): self.set(left, top, right, bottom) else: self.left = min(self.left, left) self.top = min(self.top, top) self.right = max(self.right, right) self.bottom = max(self.bottom, bottom) def round(self, r: Rect): r.left = int(self.left + 0.5) r.top = int(self.top + 0.5) r.right = int(self.right + 0.5) r.bottom = int(self.bottom + 0.5) def roundOut(self, r: Rect): r.left = int(math.floor(self.left)) r.top = int(math.floor(self.top)) r.right = int(math.ceil(self.right)) r.bottom = int(math.ceil(self.bottom)) def sort(self): if self.left > self.right: self.left, self.right = self.right, self.left if self.top > self.bottom: self.top, self.bottom = self.bottom, self.top def isEmpty(self) -> bool: return self.left >= self.right or self.top >= self.bottom def setEmpty(self): self.left = self.top = self.right = self.bottom = 0.0 def toShortString(self) -> str: return f"[{self.left:.1f},{self.top:.1f}][{self.right:.1f},{self.bottom:.1f}]" def toString(self) -> str: return f"RectF({self.left}, {self.top}, {self.right}, {self.bottom})" # ═══════════════════════════════════════════════════════════════ # Point / PointF # ═══════════════════════════════════════════════════════════════ class Point(Stub): """Point — 整数坐标点""" def __init__(self, x: int = 0, y: int = 0): super().__init__() self.x = x self.y = y def set(self, x: int, y: int): self.x, self.y = x, y def offset(self, dx: int, dy: int): self.x += dx self.y += dy def equals(self, other) -> bool: if isinstance(other, Point): return self.x == other.x and self.y == other.y return False def hashCode(self) -> int: return self.x * 32713 + self.y def toString(self) -> str: return f"Point({self.x}, {self.y})" class PointF(Stub): """PointF — 浮点坐标点""" def __init__(self, x: float = 0.0, y: float = 0.0): super().__init__() self.x = x self.y = y def set(self, x: float, y: float): self.x, self.y = x, y def offset(self, dx: float, dy: float): self.x += dx self.y += dy def equals(self, other) -> bool: if isinstance(other, PointF): return self.x == other.x and self.y == other.y return False def hashCode(self) -> int: return int(self.x * 32713 + self.y) def toString(self) -> str: return f"PointF({self.x}, {self.y})" # ═══════════════════════════════════════════════════════════════ # Path # ═══════════════════════════════════════════════════════════════ class Path(Stub): """Path — 路径""" class Direction: CW = 0 CCW = 1 class FillType: WINDING = 0 EVEN_ODD = 1 INVERSE_WINDING = 2 INVERSE_EVEN_ODD = 3 class Op: DIFFERENCE = 0 INTERSECT = 1 UNION = 2 XOR = 3 REVERSE_DIFFERENCE = 4 def __init__(self): super().__init__() self._segments: List[dict] = [] self._fill_type = Path.FillType.WINDING self._start_x = 0.0 self._start_y = 0.0 self._current_x = 0.0 self._current_y = 0.0 def moveTo(self, x: float, y: float): self._segments.append({"type": "move", "x": x, "y": y}) self._current_x = x self._current_y = y def lineTo(self, x: float, y: float): self._segments.append({"type": "line", "x": x, "y": y}) self._current_x = x self._current_y = y def quadTo(self, x1: float, y1: float, x2: float, y2: float): self._segments.append({"type": "quad", "x1": x1, "y1": y1, "x2": x2, "y2": y2}) self._current_x = x2 self._current_y = y2 def cubicTo(self, x1: float, y1: float, x2: float, y2: float, x3: float, y3: float): self._segments.append({"type": "cubic", "x1": x1, "y1": y1, "x2": x2, "y2": y2, "x3": x3, "y3": y3}) self._current_x = x3 self._current_y = y3 def rMoveTo(self, dx: float, dy: float): self.moveTo(self._current_x + dx, self._current_y + dy) def rLineTo(self, dx: float, dy: float): self.lineTo(self._current_x + dx, self._current_y + dy) def rQuadTo(self, dx1: float, dy1: float, dx2: float, dy2: float): self.quadTo(self._current_x + dx1, self._current_y + dy1, self._current_x + dx2, self._current_y + dy2) def rCubicTo(self, dx1: float, dy1: float, dx2: float, dy2: float, dx3: float, dy3: float): self.cubicTo(self._current_x + dx1, self._current_y + dy1, self._current_x + dx2, self._current_y + dy2, self._current_x + dx3, self._current_y + dy3) def addRect(self, left: float, top: float, right: float, bottom: float, dir: int = Direction.CW): self.moveTo(left, top) self.lineTo(right, top) self.lineTo(right, bottom) self.lineTo(left, bottom) self.close() def addRoundRect(self, rect: Union["RectF", tuple], rx: float, ry: float, dir: int = Direction.CW): if isinstance(rect, RectF): l, t, r, b = rect.left, rect.top, rect.right, rect.bottom else: l, t, r, b = rect self.moveTo(l + rx, t) self.lineTo(r - rx, t) self.quadTo(r, t, r, t + ry) self.lineTo(r, b - ry) self.quadTo(r, b, r - rx, b) self.lineTo(l + rx, b) self.quadTo(l, b, l, b - ry) self.lineTo(l, t + ry) self.quadTo(l, t, l + rx, t) self.close() def addCircle(self, x: float, y: float, radius: float, dir: int = Direction.CW): k = 0.5522847498 self.moveTo(x + radius, y) self.cubicTo(x + radius, y + radius * k, x + radius * k, y + radius, x, y + radius) self.cubicTo(x - radius * k, y + radius, x - radius, y + radius * k, x - radius, y) self.cubicTo(x - radius, y - radius * k, x - radius * k, y - radius, x, y - radius) self.cubicTo(x + radius * k, y - radius, x + radius, y - radius * k, x + radius, y) self.close() def addOval(self, rect: Union["RectF", tuple], dir: int = Direction.CW): if isinstance(rect, RectF): l, t, r, b = rect.left, rect.top, rect.right, rect.bottom else: l, t, r, b = rect cx = (l + r) / 2.0 cy = (t + b) / 2.0 rx = (r - l) / 2.0 ry = (b - t) / 2.0 kx = rx * 0.5522847498 ky = ry * 0.5522847498 self.moveTo(cx + rx, cy) self.cubicTo(cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry) self.cubicTo(cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy) self.cubicTo(cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry) self.cubicTo(cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy) self.close() def addArc(self, rect: Union["RectF", tuple], startAngle: float, sweepAngle: float): if isinstance(rect, RectF): l, t, r, b = rect.left, rect.top, rect.right, rect.bottom else: l, t, r, b = rect cx = (l + r) / 2.0 cy = (t + b) / 2.0 rx = (r - l) / 2.0 ry = (b - t) / 2.0 start_rad = math.radians(startAngle) end_rad = math.radians(startAngle + sweepAngle) segments = 4 da = (end_rad - start_rad) / segments for i in range(segments + 1): a = start_rad + i * da px = cx + rx * math.cos(a) py = cy + ry * math.sin(a) if i == 0: self.moveTo(px, py) else: self.lineTo(px, py) def addPath(self, path: "Path", dx: float = 0.0, dy: float = 0.0): for seg in path._segments: s = dict(seg) if "x" in s: s["x"] = s["x"] + dx if "y" in s: s["y"] = s["y"] + dy if "x1" in s: s["x1"] = s["x1"] + dx if "y1" in s: s["y1"] = s["y1"] + dy if "x2" in s: s["x2"] = s["x2"] + dx if "y2" in s: s["y2"] = s["y2"] + dy if "x3" in s: s["x3"] = s["x3"] + dx if "y3" in s: s["y3"] = s["y3"] + dy self._segments.append(s) self._current_x = path._current_x + dx self._current_y = path._current_y + dy def arcTo(self, rect: Union["RectF", tuple], startAngle: float, sweepAngle: float, forceMoveTo: bool = False): if isinstance(rect, RectF): l, t, r, b = rect.left, rect.top, rect.right, rect.bottom else: l, t, r, b = rect cx = (l + r) / 2.0 cy = (t + b) / 2.0 rx = (r - l) / 2.0 ry = (b - t) / 2.0 start_rad = math.radians(startAngle) end_rad = math.radians(startAngle + sweepAngle) segments = 4 da = (end_rad - start_rad) / segments for i in range(segments + 1): a = start_rad + i * da px = cx + rx * math.cos(a) py = cy + ry * math.sin(a) if i == 0 and forceMoveTo: self.moveTo(px, py) elif i == 0: self.lineTo(px, py) else: self.lineTo(px, py) def close(self): self._segments.append({"type": "close"}) def reset(self): self._segments.clear() self._current_x = 0.0 self._current_y = 0.0 def rewind(self): self._segments.clear() self._current_x = 0.0 self._current_y = 0.0 def set(self, src: "Path"): self._segments = list(src._segments) self._fill_type = src._fill_type self._current_x = src._current_x self._current_y = src._current_y def computeBounds(self, exact: bool = False) -> RectF: if not self._segments: return RectF(0, 0, 0, 0) min_x = float("inf") min_y = float("inf") max_x = float("-inf") max_y = float("-inf") for seg in self._segments: for key in ("x", "x1", "x2", "x3"): if key in seg: if seg[key] < min_x: min_x = seg[key] if seg[key] > max_x: max_x = seg[key] for key in ("y", "y1", "y2", "y3"): if key in seg: if seg[key] < min_y: min_y = seg[key] if seg[key] > max_y: max_y = seg[key] if min_x == float("inf"): return RectF(0, 0, 0, 0) return RectF(min_x, min_y, max_x, max_y) def incReserve(self, extraPtCount: int): pass def op(self, other: "Path", op: int) -> bool: return False def isRect(self) -> bool: if len(self._segments) != 5: return False types = [s["type"] for s in self._segments] return types == ["move", "line", "line", "line", "close"] def isConvex(self) -> bool: return True def setFillType(self, ft: int): self._fill_type = ft def getFillType(self) -> int: return self._fill_type def toggleReverseFillType(self): mapping = { Path.FillType.WINDING: Path.FillType.INVERSE_WINDING, Path.FillType.EVEN_ODD: Path.FillType.INVERSE_EVEN_ODD, Path.FillType.INVERSE_WINDING: Path.FillType.WINDING, Path.FillType.INVERSE_EVEN_ODD: Path.FillType.EVEN_ODD, } self._fill_type = mapping.get(self._fill_type, Path.FillType.WINDING) # ═══════════════════════════════════════════════════════════════ # PathMeasure # ═══════════════════════════════════════════════════════════════ class PathMeasure(Stub): """PathMeasure — 路径测量""" def __init__(self, path: Path = None, forceClosed: bool = False): super().__init__() self._path = path self._force_closed = forceClosed def setPath(self, path: Path, forceClosed: bool): self._path = path self._force_closed = forceClosed def getLength(self) -> float: if not self._path: return 0.0 bounds = self._path.computeBounds() return math.sqrt(bounds.width() ** 2 + bounds.height() ** 2) def getPosTan(self, distance: float, pos: List[float], tan: List[float]) -> bool: if not self._path: return False if len(pos) >= 2: pos[0] = 0.0 pos[1] = 0.0 if len(tan) >= 2: tan[0] = 1.0 tan[1] = 0.0 return True def getMatrix(self, distance: float, matrix: "Matrix", flags: int) -> bool: return False def getSegment(self, startD: float, stopD: float, dst: Path, startWithMoveTo: bool) -> bool: return False def isClosed(self) -> bool: return self._force_closed def nextContour(self) -> bool: return False # ═══════════════════════════════════════════════════════════════ # PorterDuff # ═══════════════════════════════════════════════════════════════ class PorterDuff(Stub): """PorterDuff — 图形混合模式""" class Mode: CLEAR = 0 SRC = 1 DST = 2 SRC_OVER = 3 DST_OVER = 4 SRC_IN = 5 DST_IN = 6 SRC_OUT = 7 DST_OUT = 8 SRC_ATOP = 9 DST_ATOP = 10 XOR = 11 ADD = 12 MULTIPLY = 13 SCREEN = 14 OVERLAY = 15 DARKEN = 16 LIGHTEN = 17 # ═══════════════════════════════════════════════════════════════ # ClipOp # ═══════════════════════════════════════════════════════════════ class ClipOp(Stub): """ClipOp — 裁剪操作""" DIFFERENCE = 0 INTERSECT = 1 REPLACE = 2 # ═══════════════════════════════════════════════════════════════ # BlendMode # ═══════════════════════════════════════════════════════════════ class BlendMode(Stub): """BlendMode — Android 29+ 混合模式""" CLEAR = 0 SRC = 1 DST = 2 SRC_OVER = 3 DST_OVER = 4 SRC_IN = 5 DST_IN = 6 SRC_OUT = 7 DST_OUT = 8 SRC_ATOP = 9 DST_ATOP = 10 XOR = 11 PLUS = 12 MODULATE = 13 SCREEN = 14 OVERLAY = 15 DARKEN = 16 LIGHTEN = 17 # ═══════════════════════════════════════════════════════════════ # BlendModeColorFilter # ═══════════════════════════════════════════════════════════════ class BlendModeColorFilter(Stub): """BlendModeColorFilter — 混合模式颜色过滤器""" def __init__(self, color: int, mode): super().__init__() self._color = color self._mode = mode def getColor(self) -> int: return self._color def getMode(self): return self._mode # ═══════════════════════════════════════════════════════════════ # ColorFilter # ═══════════════════════════════════════════════════════════════ class ColorFilter(Stub): """ColorFilter — 颜色过滤器的抽象基类""" pass # ═══════════════════════════════════════════════════════════════ # PorterDuffColorFilter # ═══════════════════════════════════════════════════════════════ class PorterDuffColorFilter(ColorFilter): """PorterDuffColorFilter — PorterDuff 颜色过滤器""" def __init__(self, color: int, mode): super().__init__() self._color = color self._mode = mode def getColor(self) -> int: return self._color def getMode(self): return self._mode # ═══════════════════════════════════════════════════════════════ # LightingColorFilter # ═══════════════════════════════════════════════════════════════ class LightingColorFilter(ColorFilter): """LightingColorFilter — 光照颜色过滤器""" def __init__(self, mul: int, add: int): super().__init__() self._mul = mul self._add = add def getMultiply(self) -> int: return self._mul def getAdd(self) -> int: return self._add # ═══════════════════════════════════════════════════════════════ # ComposeColorFilter # ═══════════════════════════════════════════════════════════════ class ComposeColorFilter(ColorFilter): """ComposeColorFilter — 组合颜色过滤器""" def __init__(self, cf1: ColorFilter, cf2: ColorFilter): super().__init__() self._cf1 = cf1 self._cf2 = cf2 def getColorFilters(self) -> Tuple[ColorFilter, ColorFilter]: return (self._cf1, self._cf2) # ═══════════════════════════════════════════════════════════════ # ColorMatrix # ═══════════════════════════════════════════════════════════════ class ColorMatrix(Stub): """ColorMatrix — 4x5 颜色变换矩阵""" def __init__(self, src: "ColorMatrix" = None): super().__init__() if src: self._array = list(src._array) else: self._array = [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0] def getArray(self) -> List[float]: return self._array def set(self, src: "ColorMatrix"): self._array = list(src._array) def reset(self): self._array = [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0] def setSaturation(self, sat: float): R_lum = 0.213 G_lum = 0.715 B_lum = 0.072 one_minus = 1.0 - sat self._array = [ R_lum * one_minus + sat, G_lum * one_minus, B_lum * one_minus, 0, 0, R_lum * one_minus, G_lum * one_minus + sat, B_lum * one_minus, 0, 0, R_lum * one_minus, G_lum * one_minus, B_lum * one_minus + sat, 0, 0, 0, 0, 0, 1, 0, ] def setRotate(self, axis: int, degrees: float): rad = math.radians(degrees) c = math.cos(rad) s = math.sin(rad) self.reset() if axis == 0: self._array[5] = c self._array[6] = s self._array[9] = -s self._array[10] = c elif axis == 1: self._array[0] = c self._array[2] = -s self._array[8] = s self._array[10] = c elif axis == 2: self._array[0] = c self._array[1] = s self._array[4] = -s self._array[5] = c def setConcat(self, a: "ColorMatrix", b: "ColorMatrix"): a_arr = a._array b_arr = b._array result = [0.0] * 20 for i in range(4): for j in range(5): s = 0.0 for k in range(4): s += a_arr[i * 5 + k] * b_arr[k * 5 + j] result[i * 5 + j] = s self._array = result def preConcat(self, other: "ColorMatrix"): new = ColorMatrix() new.setConcat(self, other) self._array = new._array def postConcat(self, other: "ColorMatrix"): new = ColorMatrix() new.setConcat(other, self) self._array = new._array def setScale(self, rScale: float, gScale: float, bScale: float, aScale: float): self._array = [ rScale, 0, 0, 0, 0, 0, gScale, 0, 0, 0, 0, 0, bScale, 0, 0, 0, 0, 0, aScale, 0, ] # ═══════════════════════════════════════════════════════════════ # ColorMatrixColorFilter # ═══════════════════════════════════════════════════════════════ class ColorMatrixColorFilter(ColorFilter): """ColorMatrixColorFilter — 颜色矩阵过滤器""" def __init__(self, matrix: ColorMatrix): super().__init__() self._matrix = matrix def getMatrix(self) -> ColorMatrix: return self._matrix # ═══════════════════════════════════════════════════════════════ # Matrix # ═══════════════════════════════════════════════════════════════ class Matrix(Stub): """Matrix — 3x3 变换矩阵""" IDENTITY_MATRIX: "Matrix" = None MPERSP_0 = 6 MPERSP_1 = 7 MPERSP_2 = 8 MSCALE_X = 0 MSCALE_Y = 4 MSKEW_X = 1 MSKEW_Y = 3 MTRANS_X = 2 MTRANS_Y = 5 def __init__(self, src: "Matrix" = None): super().__init__() if src: self._values = list(src._values) else: self._values = [1, 0, 0, 0, 1, 0, 0, 0, 1] def getValues(self) -> List[float]: return self._values def setValues(self, values: List[float]): self._values = list(values) def reset(self): self._values = [1, 0, 0, 0, 1, 0, 0, 0, 1] def isIdentity(self) -> bool: return self._values == [1, 0, 0, 0, 1, 0, 0, 0, 1] def invert(self, inverse: "Matrix") -> bool: v = self._values det = v[0] * (v[4] * v[8] - v[5] * v[7]) \ - v[1] * (v[3] * v[8] - v[5] * v[6]) \ + v[2] * (v[3] * v[7] - v[4] * v[6]) if abs(det) < 1e-10: return False inv_det = 1.0 / det inv = [ (v[4] * v[8] - v[5] * v[7]) * inv_det, (v[2] * v[7] - v[1] * v[8]) * inv_det, (v[1] * v[5] - v[2] * v[4]) * inv_det, (v[5] * v[6] - v[3] * v[8]) * inv_det, (v[0] * v[8] - v[2] * v[6]) * inv_det, (v[2] * v[3] - v[0] * v[5]) * inv_det, (v[3] * v[7] - v[4] * v[6]) * inv_det, (v[1] * v[6] - v[0] * v[7]) * inv_det, (v[0] * v[4] - v[1] * v[3]) * inv_det, ] inverse._values = inv return True def setTranslate(self, dx: float, dy: float): self._values = [1, 0, dx, 0, 1, dy, 0, 0, 1] def setScale(self, sx: float, sy: float, px: float = 0.0, py: float = 0.0): self._values = [sx, 0, px - sx * px, 0, sy, py - sy * py, 0, 0, 1] def setRotate(self, degrees: float, px: float = 0.0, py: float = 0.0): rad = math.radians(degrees) c = math.cos(rad) s = math.sin(rad) tx = px - px * c + py * s ty = py - px * s - py * c self._values = [c, -s, tx, s, c, ty, 0, 0, 1] def setSkew(self, kx: float, ky: float, px: float = 0.0, py: float = 0.0): self._values = [1, kx, -kx * px, ky, 1, -ky * py, 0, 0, 1] def setConcat(self, a: "Matrix", b: "Matrix"): va, vb = a._values, b._values self._values = [ va[0] * vb[0] + va[1] * vb[3] + va[2] * vb[6], va[0] * vb[1] + va[1] * vb[4] + va[2] * vb[7], va[0] * vb[2] + va[1] * vb[5] + va[2] * vb[8], va[3] * vb[0] + va[4] * vb[3] + va[5] * vb[6], va[3] * vb[1] + va[4] * vb[4] + va[5] * vb[7], va[3] * vb[2] + va[4] * vb[5] + va[5] * vb[8], va[6] * vb[0] + va[7] * vb[3] + va[8] * vb[6], va[6] * vb[1] + va[7] * vb[4] + va[8] * vb[7], va[6] * vb[2] + va[7] * vb[5] + va[8] * vb[8], ] def preConcat(self, other: "Matrix"): new = Matrix() new.setConcat(self, other) self._values = new._values def postConcat(self, other: "Matrix"): new = Matrix() new.setConcat(other, self) self._values = new._values def preTranslate(self, dx: float, dy: float): m = Matrix() m.setTranslate(dx, dy) self.preConcat(m) def preScale(self, sx: float, sy: float, px: float = 0.0, py: float = 0.0): m = Matrix() m.setScale(sx, sy, px, py) self.preConcat(m) def preRotate(self, degrees: float, px: float = 0.0, py: float = 0.0): m = Matrix() m.setRotate(degrees, px, py) self.preConcat(m) def preSkew(self, kx: float, ky: float, px: float = 0.0, py: float = 0.0): m = Matrix() m.setSkew(kx, ky, px, py) self.preConcat(m) def postTranslate(self, dx: float, dy: float): m = Matrix() m.setTranslate(dx, dy) self.postConcat(m) def postScale(self, sx: float, sy: float, px: float = 0.0, py: float = 0.0): m = Matrix() m.setScale(sx, sy, px, py) self.postConcat(m) def postRotate(self, degrees: float, px: float = 0.0, py: float = 0.0): m = Matrix() m.setRotate(degrees, px, py) self.postConcat(m) def postSkew(self, kx: float, ky: float, px: float = 0.0, py: float = 0.0): m = Matrix() m.setSkew(kx, ky, px, py) self.postConcat(m) def setRectToRect(self, src: RectF, dst: RectF, stf: int) -> bool: if src.isEmpty() or dst.isEmpty(): return False sx = dst.width() / src.width() sy = dst.height() / src.height() tx = dst.left - src.left * sx ty = dst.top - src.top * sy self._values = [sx, 0, tx, 0, sy, ty, 0, 0, 1] return True def mapRect(self, src: RectF, dst: RectF = None) -> RectF: pts = [src.left, src.top, src.right, src.bottom] self.mapPoints(pts) if dst: dst.set(pts[0], pts[1], pts[2], pts[3]) return dst return RectF(pts[0], pts[1], pts[2], pts[3]) def mapPoints(self, pts: List[float], offset: int = 0, count: int = -1): if count < 0: count = len(pts) - offset v = self._values for i in range(offset, offset + count, 2): x = pts[i] y = pts[i + 1] pts[i] = x * v[0] + y * v[1] + v[2] pts[i + 1] = x * v[3] + y * v[4] + v[5] def mapVectors(self, vecs: List[float]): v = self._values for i in range(0, len(vecs), 2): x = vecs[i] y = vecs[i + 1] vecs[i] = x * v[0] + y * v[1] vecs[i + 1] = x * v[3] + y * v[4] def mapRadius(self, radius: float) -> float: v = self._values sx = math.sqrt(v[0] * v[0] + v[3] * v[3]) sy = math.sqrt(v[1] * v[1] + v[4] * v[4]) return radius * max(sx, sy) def toString(self) -> str: return f"Matrix({self._values})" Matrix.IDENTITY_MATRIX = Matrix() # ═══════════════════════════════════════════════════════════════ # Shader # ═══════════════════════════════════════════════════════════════ class Shader(Stub): """Shader — 着色器基类""" class TileMode: CLAMP = 0 REPEAT = 1 MIRROR = 2 def __init__(self): super().__init__() self._local_matrix = None def setLocalMatrix(self, matrix: Matrix): self._local_matrix = matrix def getLocalMatrix(self) -> Optional[Matrix]: return self._local_matrix # ═══════════════════════════════════════════════════════════════ # LinearGradient # ═══════════════════════════════════════════════════════════════ class LinearGradient(Shader): """LinearGradient — 线性渐变""" def __init__(self, x0: float, y0: float, x1: float, y1: float, colors: Sequence[int], positions: Sequence[float], tile: int = Shader.TileMode.CLAMP): super().__init__() self._x0, self._y0 = x0, y0 self._x1, self._y1 = x1, y1 self._colors = list(colors) self._positions = list(positions) if positions else None self._tile = tile # ═══════════════════════════════════════════════════════════════ # RadialGradient # ═══════════════════════════════════════════════════════════════ class RadialGradient(Shader): """RadialGradient — 径向渐变""" CENTER_COLOR = 0 EDGE_COLOR = 1 def __init__(self, x: float, y: float, radius: float, colors: Sequence[int], positions: Sequence[float], tile: int = Shader.TileMode.CLAMP, centerColor: int = Shader.TileMode.CLAMP): """centerColor is ignored, kept for API compatibility""" super().__init__() self._x, self._y = x, y self._radius = radius self._colors = list(colors) self._positions = list(positions) if positions else None self._tile = tile def getX(self) -> float: return self._x def getY(self) -> float: return self._y def getRadius(self) -> float: return self._radius # ═══════════════════════════════════════════════════════════════ # SweepGradient # ═══════════════════════════════════════════════════════════════ class SweepGradient(Shader): """SweepGradient — 扫描渐变""" def __init__(self, x: float, y: float, colors: Sequence[int], positions: Sequence[float]): super().__init__() self._x, self._y = x, y self._colors = list(colors) self._positions = list(positions) if positions else None def getX(self) -> float: return self._x def getY(self) -> float: return self._y # ═══════════════════════════════════════════════════════════════ # BitmapShader # ═══════════════════════════════════════════════════════════════ class BitmapShader(Shader): """BitmapShader — 位图着色器""" def __init__(self, bitmap: "Bitmap", tileX: int, tileY: int): super().__init__() self._bitmap = bitmap self._tile_x = tileX self._tile_y = tileY def getBitmap(self) -> "Bitmap": return self._bitmap # ═══════════════════════════════════════════════════════════════ # ComposeShader # ═══════════════════════════════════════════════════════════════ class ComposeShader(Shader): """ComposeShader — 组合着色器""" def __init__(self, shaderA: Shader, shaderB: Shader, mode): super().__init__() self._shader_a = shaderA self._shader_b = shaderB self._mode = mode # ═══════════════════════════════════════════════════════════════ # MaskFilter / BlurMaskFilter / EmbossMaskFilter # ═══════════════════════════════════════════════════════════════ class MaskFilter(Stub): """MaskFilter — 遮罩过滤器基类""" pass class BlurMaskFilter(MaskFilter): """BlurMaskFilter — 模糊遮罩过滤器""" NORMAL = 0 SOLID = 1 OUTER = 2 INNER = 3 def __init__(self, radius: float, style: int): super().__init__() self._radius = radius self._style = style def getRadius(self) -> float: return self._radius def getStyle(self) -> int: return self._style class EmbossMaskFilter(MaskFilter): """EmbossMaskFilter — 浮雕遮罩过滤器""" def __init__(self, direction: List[float], ambient: float, specular: float, blurRadius: float): super().__init__() self._direction = list(direction) self._ambient = ambient self._specular = specular self._blur_radius = blurRadius # ═══════════════════════════════════════════════════════════════ # PathEffect hierarchy # ═══════════════════════════════════════════════════════════════ class PathEffect(Stub): """PathEffect — 路径效果基类""" pass class ComposePathEffect(PathEffect): """ComposePathEffect — 组合路径效果""" def __init__(self, outerpe: PathEffect, innerpe: PathEffect): super().__init__() self._outer = outerpe self._inner = innerpe class CornerPathEffect(PathEffect): """CornerPathEffect — 圆角路径效果""" def __init__(self, radius: float): super().__init__() self._radius = radius class DashPathEffect(PathEffect): """DashPathEffect — 虚线路径效果""" def __init__(self, intervals: List[float], phase: float): super().__init__() self._intervals = list(intervals) self._phase = phase class DiscretePathEffect(PathEffect): """DiscretePathEffect — 离散路径效果""" def __init__(self, segmentLength: float, deviation: float): super().__init__() self._segment_length = segmentLength self._deviation = deviation class PathDashPathEffect(PathEffect): """PathDashPathEffect — 路径虚线效果""" ROTATE = 0 MORPH = 1 TRANSLATE = 2 def __init__(self, shape: Path, advance: float, phase: float, style: int): super().__init__() self._shape = shape self._advance = advance self._phase = phase self._style = style class SumPathEffect(PathEffect): """SumPathEffect — 求和路径效果""" def __init__(self, first: PathEffect, second: PathEffect): super().__init__() self._first = first self._second = second # ═══════════════════════════════════════════════════════════════ # Xfermode hierarchy # ═══════════════════════════════════════════════════════════════ class Xfermode(Stub): """Xfermode — 传输模式基类""" pass class PorterDuffXfermode(Xfermode): """PorterDuffXfermode — PorterDuff 传输模式""" def __init__(self, mode): super().__init__() self._mode = mode def getMode(self): return self._mode class AvoidXfermode(Xfermode): """AvoidXfermode — deprecated, kept for API compatibility""" TARGET = 0 AVOID = 1 def __init__(self, opColor: int, tolerance: int, mode: int): super().__init__() self._op_color = opColor self._tolerance = tolerance self._mode = mode class PixelXorXfermode(Xfermode): """PixelXorXfermode — deprecated, kept for API compatibility""" def __init__(self, opColor: int): super().__init__() self._op_color = opColor # ═══════════════════════════════════════════════════════════════ # DrawFilter # ═══════════════════════════════════════════════════════════════ class DrawFilter(Stub): """DrawFilter — 绘制过滤器基类""" pass class PaintFlagsDrawFilter(DrawFilter): """PaintFlagsDrawFilter — 画笔标志绘制过滤器""" def __init__(self, clearBits: int, setBits: int): super().__init__() self._clear_bits = clearBits self._set_bits = setBits # ═══════════════════════════════════════════════════════════════ # Camera # ═══════════════════════════════════════════════════════════════ class Camera(Stub): """Camera — 3D 相机变换""" def __init__(self): super().__init__() self._location_x = 0.0 self._location_y = 0.0 self._location_z = -8.0 def save(self): pass def restore(self): pass def translate(self, x: float, y: float, z: float): self._location_x += x self._location_y += y self._location_z += z def rotateX(self, deg: float): pass def rotateY(self, deg: float): pass def rotateZ(self, deg: float): pass def rotate(self, x: float, y: float, z: float): pass def getMatrix(self, matrix: Matrix): matrix.reset() def applyToCanvas(self, canvas: "Canvas"): pass def setLocation(self, x: float, y: float, z: float): self._location_x = x self._location_y = y self._location_z = z def dotWithNormal(self, dx: float, dy: float, dz: float) -> float: return 0.0 # ═══════════════════════════════════════════════════════════════ # LayerRasterizer / Rasterizer # ═══════════════════════════════════════════════════════════════ class Rasterizer(Stub): """Rasterizer — 光栅化基类 (deprecated)""" pass class LayerRasterizer(Rasterizer): """LayerRasterizer — 分层光栅化 (deprecated)""" def __init__(self): super().__init__() def addLayer(self, paint: "Paint", dx: float = 0.0, dy: float = 0.0): pass # ═══════════════════════════════════════════════════════════════ # NinePatch # ═══════════════════════════════════════════════════════════════ class NinePatch(Stub): """NinePatch — 九宫格图片""" def __init__(self, bitmap: "Bitmap", chunk: bytes): super().__init__() self._bitmap = bitmap self._chunk = chunk if chunk else b"" def getBitmap(self) -> "Bitmap": return self._bitmap def getWidth(self) -> int: return self._bitmap.getWidth() if self._bitmap else 0 def getHeight(self) -> int: return self._bitmap.getHeight() if self._bitmap else 0 @staticmethod def isNinePatchChunk(chunk: bytes) -> bool: return bool(chunk and len(chunk) > 0) # ═══════════════════════════════════════════════════════════════ # Picture / PictureDrawable # ═══════════════════════════════════════════════════════════════ class Picture(Stub): """Picture — 图画录制与回放""" def __init__(self): super().__init__() self._recorded = [] def beginRecording(self, width: int, height: int) -> "Canvas": canvas = Canvas(width, height) self._canvas = canvas return canvas def endRecording(self): self._canvas = None def draw(self, canvas: "Canvas"): for cmd in self._recorded: pass def getWidth(self) -> int: return self._canvas.getWidth() if hasattr(self, "_canvas") and self._canvas else 0 def getHeight(self) -> int: return self._canvas.getHeight() if hasattr(self, "_canvas") and self._canvas else 0 def createFromStream(self, stream) -> "Picture": return Picture() def writeToStream(self, stream) -> bool: return True class PictureDrawable(Stub): """PictureDrawable — 图画 Drawable""" def __init__(self, picture: Picture): super().__init__() self._picture = picture def getPicture(self) -> Picture: return self._picture def setPicture(self, picture: Picture): self._picture = picture def draw(self, canvas: "Canvas"): pass # ═══════════════════════════════════════════════════════════════ # SurfaceTexture # ═══════════════════════════════════════════════════════════════ class SurfaceTexture(Stub): """SurfaceTexture — 表面纹理""" def __init__(self, texName: int = 0): super().__init__() self._tex_name = texName self._timestamp = 0 def getTimestamp(self) -> int: return self._timestamp def updateTexImage(self): self._timestamp = int(time.time() * 1e9) def release(self): pass def setDefaultBufferSize(self, width: int, height: int): pass def attachToGLContext(self, texName: int): pass def detachFromGLContext(self): pass # ═══════════════════════════════════════════════════════════════ # Region / RegionIterator # ═══════════════════════════════════════════════════════════════ class Region(Stub): """Region — 区域""" OP_DIFFERENCE = 0 OP_INTERSECT = 1 OP_UNION = 2 OP_XOR = 3 OP_REVERSE_DIFFERENCE = 4 OP_REPLACE = 5 def __init__(self, *args): super().__init__() if len(args) == 4: self._rect = Rect(args[0], args[1], args[2], args[3]) elif len(args) == 1 and isinstance(args[0], Rect): self._rect = Rect(args[0].left, args[0].top, args[0].right, args[0].bottom) else: self._rect = Rect() def set(self, *args) -> bool: if len(args) == 4: self._rect.set(args[0], args[1], args[2], args[3]) elif len(args) == 1 and isinstance(args[0], Rect): r = args[0] self._rect.set(r.left, r.top, r.right, r.bottom) return True def getBounds(self, r: Rect = None) -> Rect: if r: r.set(self._rect.left, self._rect.top, self._rect.right, self._rect.bottom) return r return Rect(self._rect.left, self._rect.top, self._rect.right, self._rect.bottom) def isEmpty(self) -> bool: return self._rect.isEmpty() def contains(self, x: int, y: int) -> bool: return self._rect.contains(x, y) def op(self, *args) -> bool: return False def isRect(self) -> bool: return True def isComplex(self) -> bool: return False def toString(self) -> str: return f"Region({self._rect})" class RegionIterator(Stub): """RegionIterator — 区域迭代器""" def __init__(self, region: Region): super().__init__() self._region = region self._done = False def next(self, r: Rect) -> bool: if self._done: return False r.set(self._region._rect.left, self._region._rect.top, self._region._rect.right, self._region._rect.bottom) self._done = True return True # ═══════════════════════════════════════════════════════════════ # Paint # ═══════════════════════════════════════════════════════════════ class Paint(Stub): """Paint — 画笔""" class Style: FILL = 0 STROKE = 1 FILL_AND_STROKE = 2 class Cap: BUTT = 0 ROUND = 1 SQUARE = 2 class Join: MITER = 0 ROUND = 1 BEVEL = 2 class Align: LEFT = 0 CENTER = 1 RIGHT = 2 ANTI_ALIAS_FLAG = 0x01 FILTER_BITMAP_FLAG = 0x02 DITHER_FLAG = 0x04 UNDERLINE_TEXT_FLAG = 0x08 STRIKE_THRU_TEXT_FLAG = 0x10 FAKE_BOLD_TEXT_FLAG = 0x20 LINEAR_TEXT_FLAG = 0x40 SUBPIXEL_TEXT_FLAG = 0x80 DEV_KERN_TEXT_FLAG = 0x100 ALIGN_LEFT = 0 ALIGN_CENTER = 1 ALIGN_RIGHT = 2 def __init__(self, flags: int = 0): super().__init__() self._flags = flags self._color = 0xFF000000 self._alpha = 255 self._style = Paint.Style.FILL self._stroke_width = 0.0 self._text_size = 12.0 self._typeface = None self._anti_alias = bool(flags & Paint.ANTI_ALIAS_FLAG) self._color_filter = None self._mask_filter = None self._shader = None self._path_effect = None self._xfermode = None self._filter_bitmap = bool(flags & Paint.FILTER_BITMAP_FLAG) self._dither = bool(flags & Paint.DITHER_FLAG) self._subpixel_text = bool(flags & Paint.SUBPIXEL_TEXT_FLAG) self._underline_text = bool(flags & Paint.UNDERLINE_TEXT_FLAG) self._strike_thru_text = bool(flags & Paint.STRIKE_THRU_TEXT_FLAG) self._text_align = Paint.Align.LEFT self._text_scale_x = 1.0 self._text_skew_x = 0.0 self._shadow_layer = None def setColor(self, color: int): self._color = color self._alpha = (color >> 24) & 0xFF def getColor(self) -> int: return self._color def setARGB(self, a: int, r: int, g: int, b: int): self.setColor(Color.argb(a, r, g, b)) def setAlpha(self, alpha: int): self._alpha = max(0, min(255, alpha)) self._color = (self._color & 0x00FFFFFF) | (self._alpha << 24) def getAlpha(self) -> int: return self._alpha def setStyle(self, style: int): self._style = style def getStyle(self) -> int: return self._style def setStrokeWidth(self, width: float): self._stroke_width = width def getStrokeWidth(self) -> float: return self._stroke_width def setTextSize(self, textSize: float): self._text_size = textSize def getTextSize(self) -> float: return self._text_size def setTypeface(self, typeface: Optional[Typeface]): self._typeface = typeface def getTypeface(self) -> Optional[Typeface]: return self._typeface def setAntiAlias(self, aa: bool): self._anti_alias = aa def isAntiAlias(self) -> bool: return self._anti_alias def setColorFilter(self, filter: Optional[ColorFilter]): self._color_filter = filter def getColorFilter(self) -> Optional[ColorFilter]: return self._color_filter def setMaskFilter(self, maskfilter: Optional[MaskFilter]): self._mask_filter = maskfilter def getMaskFilter(self) -> Optional[MaskFilter]: return self._mask_filter def setShader(self, shader: Optional[Shader]): self._shader = shader def getShader(self) -> Optional[Shader]: return self._shader def setPathEffect(self, effect: Optional[PathEffect]): self._path_effect = effect def getPathEffect(self) -> Optional[PathEffect]: return self._path_effect def setXfermode(self, xfermode: Optional[Xfermode]): self._xfermode = xfermode def getXfermode(self) -> Optional[Xfermode]: return self._xfermode def setFlags(self, flags: int): self._flags = flags def getFlags(self) -> int: return self._flags def setFilterBitmap(self, filter: bool): self._filter_bitmap = filter def isFilterBitmap(self) -> bool: return self._filter_bitmap def setDither(self, dither: bool): self._dither = dither def isDither(self) -> bool: return self._dither def setSubpixelText(self, subpixel: bool): self._subpixel_text = subpixel def isSubpixelText(self) -> bool: return self._subpixel_text def setUnderlineText(self, underline: bool): self._underline_text = underline def isUnderlineText(self) -> bool: return self._underline_text def setStrikeThruText(self, strike: bool): self._strike_thru_text = strike def isStrikeThruText(self) -> bool: return self._strike_thru_text def setTextScaleX(self, scaleX: float): self._text_scale_x = scaleX def getTextScaleX(self) -> float: return self._text_scale_x def setTextSkewX(self, skewX: float): self._text_skew_x = skewX def getTextSkewX(self) -> float: return self._text_skew_x def setTextAlign(self, align: int): self._text_align = align def getTextAlign(self) -> int: return self._text_align def setTextLocales(self, locales): pass def setShadowLayer(self, radius: float, dx: float, dy: float, color: int): self._shadow_layer = (radius, dx, dy, color) def clearShadowLayer(self): self._shadow_layer = None def measureText(self, text: str) -> float: return len(text) * self._text_size * 0.5 def breakText(self, text: str, maxWidth: float, measuredWidth: List[float]) -> int: width = 0.0 count = 0 for i, ch in enumerate(text): cw = self._text_size * 0.5 if width + cw > maxWidth: break width += cw count += 1 measuredWidth[0] = width return count def getTextWidths(self, text: str, widths: List[float]) -> int: cw = self._text_size * 0.5 for i, ch in enumerate(text): widths[i] = cw return len(text) def getTextPath(self, text: str, start: int, end: int, x: float, y: float, path: Path): path.moveTo(x, y) for i in range(start, end): path.lineTo(x + (i - start + 1) * self._text_size * 0.3, y) def getFontMetrics(self) -> Dict[str, float]: size = self._text_size return { "ascent": -size * 0.8, "descent": size * 0.2, "top": -size * 0.95, "bottom": size * 0.25, "leading": 0.0, } def getFontMetricsInt(self) -> Dict[str, int]: fm = self.getFontMetrics() return {k: int(v) for k, v in fm.items()} def ascent(self) -> float: return -self._text_size * 0.8 def descent(self) -> float: return self._text_size * 0.2 # ═══════════════════════════════════════════════════════════════ # Bitmap # ═══════════════════════════════════════════════════════════════ class Bitmap(Stub): """Bitmap — 位图""" class Config: ALPHA_8 = "ALPHA_8" ARGB_4444 = "ARGB_4444" ARGB_8888 = "ARGB_8888" RGB_565 = "RGB_565" RGBA_F16 = "RGBA_F16" class CompressFormat: JPEG = "JPEG" PNG = "PNG" WEBP = "WEBP" WEBP_LOSSY = "WEBP_LOSSY" WEBP_LOSSLESS = "WEBP_LOSSLESS" def __init__(self, width: int, height: int, config: str = Config.ARGB_8888): super().__init__() self._width = width self._height = height self._config = config self._recycled = False self._has_alpha = config in (Bitmap.Config.ARGB_8888, Bitmap.Config.ARGB_4444, Bitmap.Config.ALPHA_8, Bitmap.Config.RGBA_F16) self._generation_id = 1 if HAS_PIL: from PIL import Image if config == Bitmap.Config.ALPHA_8: mode = "L" elif config == Bitmap.Config.RGB_565: mode = "RGB" else: mode = "RGBA" self._image = Image.new(mode, (width, height), (0, 0, 0, 0)) else: bpp = 4 if config == Bitmap.Config.ARGB_8888 else ( 2 if config in (Bitmap.Config.ARGB_4444, Bitmap.Config.RGB_565) else 1 ) self._pixels = bytearray(width * height * bpp) @staticmethod def createBitmap(width, height=None, config=None): if isinstance(width, Bitmap): src = width dst = Bitmap(src._width, src._height, src._config) if HAS_PIL and hasattr(src, "_image"): from PIL import Image dst._image = src._image.copy() elif hasattr(src, "_pixels"): dst._pixels = bytearray(src._pixels) return dst return Bitmap(width, height, config) @staticmethod def createScaledBitmap(src: "Bitmap", dstWidth: int, dstHeight: int, filter: bool = True) -> "Bitmap": if HAS_PIL and hasattr(src, "_image"): from PIL import Image dst = Bitmap(dstWidth, dstHeight, src._config) dst._image = src._image.resize((dstWidth, dstHeight), Image.LANCZOS if filter else Image.NEAREST) return dst dst = Bitmap(dstWidth, dstHeight, src._config) return dst def copy(self, config: str, isMutable: bool) -> "Bitmap": dst = Bitmap(self._width, self._height, config) if HAS_PIL and hasattr(self, "_image"): from PIL import Image if config != self._config: mode_map = {Bitmap.Config.ARGB_8888: "RGBA", Bitmap.Config.RGB_565: "RGB", Bitmap.Config.ALPHA_8: "L"} dst._image = self._image.convert(mode_map.get(config, "RGBA")) else: dst._image = self._image.copy() elif hasattr(self, "_pixels"): dst._pixels = bytearray(self._pixels) return dst def compress(self, format: str, quality: int, stream) -> bool: if self._recycled: return False if HAS_PIL and hasattr(self, "_image"): pil_fmt = format.lower() if pil_fmt == "jpeg": pil_fmt = "jpeg" elif pil_fmt == "webp": pil_fmt = "webp" else: pil_fmt = "png" if pil_fmt == "jpeg" and self._image.mode == "RGBA": self._image = self._image.convert("RGB") self._image.save(stream, format=pil_fmt.upper(), quality=quality) return True return False def recycle(self): self._recycled = True if HAS_PIL and hasattr(self, "_image"): self._image = None if hasattr(self, "_pixels"): self._pixels = None def isRecycled(self) -> bool: return self._recycled def getWidth(self) -> int: return self._width def getHeight(self) -> int: return self._height def getConfig(self) -> str: return self._config def getRowBytes(self) -> int: bpp = {"ALPHA_8": 1, "ARGB_4444": 2, "ARGB_8888": 4, "RGB_565": 2, "RGBA_F16": 8} return self._width * bpp.get(self._config, 4) def getByteCount(self) -> int: return self.getRowBytes() * self._height def getAllocationByteCount(self) -> int: return self.getByteCount() def getPixel(self, x: int, y: int) -> int: if self._recycled: return 0 if HAS_PIL and hasattr(self, "_image"): p = self._image.getpixel((x, y)) if self._image.mode == "RGBA": return Color.argb(p[3], p[0], p[1], p[2]) elif self._image.mode == "RGB": return Color.rgb(p[0], p[1], p[2]) elif self._image.mode == "L": return Color.argb(255, p, p, p) return 0 def getPixels(self, pixels: List[int], offset: int, stride: int, x: int, y: int, width: int, height: int): if self._recycled: return for row in range(height): for col in range(width): px = x + col py = y + row idx = offset + row * stride + col if 0 <= idx < len(pixels) and 0 <= px < self._width and 0 <= py < self._height: pixels[idx] = self.getPixel(px, py) def setPixel(self, x: int, y: int, color: int): if self._recycled: return if HAS_PIL and hasattr(self, "_image"): if self._image.mode == "RGBA": self._image.putpixel((x, y), ( Color.red(color), Color.green(color), Color.blue(color), Color.alpha(color) )) elif self._image.mode == "RGB": self._image.putpixel((x, y), ( Color.red(color), Color.green(color), Color.blue(color) )) elif self._image.mode == "L": self._image.putpixel((x, y), Color.red(color)) else: pass def setPixels(self, pixels: List[int], offset: int, stride: int, x: int, y: int, width: int, height: int): for row in range(height): for col in range(width): idx = offset + row * stride + col if idx < len(pixels): self.setPixel(x + col, y + row, pixels[idx]) def eraseColor(self, color: int): if self._recycled: return if HAS_PIL and hasattr(self, "_image"): from PIL import Image w, h = self._image.size if self._image.mode == "RGBA": self._image = Image.new("RGBA", (w, h), (Color.red(color), Color.green(color), Color.blue(color), Color.alpha(color))) elif self._image.mode == "RGB": self._image = Image.new("RGB", (w, h), (Color.red(color), Color.green(color), Color.blue(color))) def extractAlpha(self, paint: Paint = None) -> "Bitmap": return Bitmap(self._width, self._height, Bitmap.Config.ALPHA_8) def sameAs(self, other: "Bitmap") -> bool: if not isinstance(other, Bitmap): return False if self._width != other._width or self._height != other._height: return False return True def prepareToDraw(self): pass def hasAlpha(self) -> bool: return self._has_alpha def setHasAlpha(self, hasAlpha: bool): self._has_alpha = hasAlpha def getGenerationId(self) -> int: return self._generation_id def describeContents(self) -> int: return 0 def writeToParcel(self, dest, flags: int): pass def copyPixelsToBuffer(self, dst): if HAS_PIL and hasattr(self, "_image"): buf = self._image.tobytes() dst.write(buf) def copyPixelsFromBuffer(self, src): if HAS_PIL and hasattr(self, "_image"): data = src.read() import io as _io buf = _io.BytesIO(data) from PIL import Image self._image = Image.open(buf) self._image = self._image.resize((self._width, self._height)) def asMutable(self) -> "Bitmap": return Bitmap.createBitmap(self) def toString(self) -> str: return f"Bitmap({self._width}x{self._height}, config={self._config})" # ═══════════════════════════════════════════════════════════════ # BitmapFactory # ═══════════════════════════════════════════════════════════════ class BitmapFactory(Stub): """BitmapFactory — 位图工厂""" class Options(Stub): """BitmapFactory.Options — 解码选项""" def __init__(self): super().__init__() self.inJustDecodeBounds = False self.outWidth = 0 self.outHeight = 0 self.inSampleSize = 1 self.inPreferredConfig = Bitmap.Config.ARGB_8888 self.inDensity = 0 self.inTargetDensity = 0 self.inScaled = True self.inMutable = False self.outMimeType = "" self.inPurgeable = False self.inInputShareable = False @staticmethod def decodeFile(pathName: str, opts: Options = None) -> Optional[Bitmap]: resolved = resolve_android_path(pathName) if not os.path.exists(resolved): return None if HAS_PIL: from PIL import Image try: img = Image.open(resolved) if opts and opts.inJustDecodeBounds: opts.outWidth, opts.outHeight = img.size opts.outMimeType = f"image/{img.format.lower() if img.format else 'png'}" return None w, h = img.size bm = Bitmap(w, h, opts.inPreferredConfig if opts else Bitmap.Config.ARGB_8888) if img.mode != "RGBA": img = img.convert("RGBA") bm._image = img return bm except Exception: return None return Bitmap(100, 100, Bitmap.Config.ARGB_8888) @staticmethod def decodeResource(res, resId: int, opts: Options = None) -> Optional[Bitmap]: return Bitmap(100, 100, Bitmap.Config.ARGB_8888) @staticmethod def decodeStream(stream, outPadding: "Rect" = None, opts: Options = None) -> Optional[Bitmap]: if stream is None: return None if HAS_PIL: from PIL import Image try: img = Image.open(stream) if opts and opts.inJustDecodeBounds: opts.outWidth, opts.outHeight = img.size return None w, h = img.size bm = Bitmap(w, h, opts.inPreferredConfig if opts else Bitmap.Config.ARGB_8888) if img.mode != "RGBA": img = img.convert("RGBA") bm._image = img return bm except Exception: return None return Bitmap(100, 100, Bitmap.Config.ARGB_8888) @staticmethod def decodeByteArray(data: bytes, offset: int, length: int, opts: Options = None) -> Optional[Bitmap]: if not data: return None if HAS_PIL: from PIL import Image import io as _io try: buf = _io.BytesIO(data[offset:offset + length]) img = Image.open(buf) if opts and opts.inJustDecodeBounds: opts.outWidth, opts.outHeight = img.size return None w, h = img.size bm = Bitmap(w, h, opts.inPreferredConfig if opts else Bitmap.Config.ARGB_8888) if img.mode != "RGBA": img = img.convert("RGBA") bm._image = img return bm except Exception: return None return Bitmap(100, 100, Bitmap.Config.ARGB_8888) @staticmethod def decodeResourceStream(res, value, stream, pad: "Rect", opts: Options = None) -> Optional[Bitmap]: return BitmapFactory.decodeStream(stream, pad, opts) # ═══════════════════════════════════════════════════════════════ # Canvas # ═══════════════════════════════════════════════════════════════ class Canvas(Stub): """Canvas — 画布""" ALL_SAVE_FLAG = 0x1F def __init__(self, width: int = 0, height: int = 0): super().__init__() if HAS_PIL: from PIL import Image, ImageDraw self._width = width self._height = height if width > 0 and height > 0: self._image = Image.new("RGBA", (width, height), (0, 0, 0, 0)) self._draw = ImageDraw.Draw(self._image) else: self._image = None self._draw = None else: self._width = width self._height = height self._pixels = None self._bitmap = None self._save_count = 0 self._matrix_stack = [Matrix()] self._clip_stack = [Rect(0, 0, width, height)] self._matrix = Matrix() def setBitmap(self, bitmap: Bitmap): self._bitmap = bitmap self._width = bitmap.getWidth() self._height = bitmap.getHeight() if HAS_PIL: from PIL import ImageDraw if hasattr(bitmap, "_image") and bitmap._image: self._image = bitmap._image self._draw = ImageDraw.Draw(self._image) def getBitmap(self) -> Optional[Bitmap]: return self._bitmap def getWidth(self) -> int: return self._width def getHeight(self) -> int: return self._height def save(self) -> int: self._save_count += 1 self._matrix_stack.append(Matrix(self._matrix)) self._clip_stack.append(Rect( self._clip_stack[-1].left, self._clip_stack[-1].top, self._clip_stack[-1].right, self._clip_stack[-1].bottom )) return self._save_count def restore(self): if len(self._matrix_stack) > 1: self._matrix = self._matrix_stack.pop() self._clip_stack.pop() self._save_count -= 1 def saveLayer(self, bounds: RectF, paint: Paint = None, saveFlags: int = ALL_SAVE_FLAG) -> int: return self.save() def getSaveCount(self) -> int: return self._save_count def restoreToCount(self, saveCount: int): while self._save_count > saveCount: self.restore() def translate(self, dx: float, dy: float): self._matrix.postTranslate(dx, dy) def scale(self, sx: float, sy: float, px: float = 0.0, py: float = 0.0): self._matrix.postScale(sx, sy, px, py) def rotate(self, degrees: float, px: float = 0.0, py: float = 0.0): self._matrix.postRotate(degrees, px, py) def skew(self, sx: float, sy: float): self._matrix.postSkew(sx, sy) def concat(self, matrix: Matrix): self._matrix.postConcat(matrix) def setMatrix(self, matrix: Matrix): self._matrix = Matrix(matrix) def getMatrix(self, ctm: Matrix = None) -> Matrix: if ctm: ctm._values = list(self._matrix._values) return ctm return Matrix(self._matrix) def getLocalTransform(self, transform: Matrix = None) -> Matrix: return self.getMatrix(transform) def getDeviceTransform(self, transform: Matrix = None) -> Matrix: if transform: transform.reset() return transform return Matrix() def clipRect(self, left: float, top: float, right: float, bottom: float, op: int = ClipOp.INTERSECT): r = self._clip_stack[-1] if op == ClipOp.INTERSECT: self._clip_stack[-1] = Rect( max(r.left, int(left)), max(r.top, int(top)), min(r.right, int(right)), min(r.bottom, int(bottom)) ) elif op == ClipOp.DIFFERENCE: pass def clipPath(self, path: Path, op: int = ClipOp.INTERSECT): bounds = path.computeBounds() self.clipRect(bounds.left, bounds.top, bounds.right, bounds.bottom, op) def clipRegion(self, region: Region, op: int = ClipOp.INTERSECT): r = region._rect self.clipRect(float(r.left), float(r.top), float(r.right), float(r.bottom), op) def getClipBounds(self, bounds: Rect = None) -> Rect: r = self._clip_stack[-1] if bounds: bounds.set(r.left, r.top, r.right, r.bottom) return bounds return Rect(r.left, r.top, r.right, r.bottom) def quickReject(self, *args) -> bool: return False def drawColor(self, color: int): if HAS_PIL and self._draw is not None: r, g, b = Color.red(color), Color.green(color), Color.blue(color) a = Color.alpha(color) self._draw.rectangle([0, 0, self._width, self._height], fill=(r, g, b, a)) def drawARGB(self, a: int, r: int, g: int, b: int): self.drawColor(Color.argb(a, r, g, b)) def drawRGB(self, r: int, g: int, b: int): self.drawARGB(255, r, g, b) def drawPaint(self, paint: Paint): self.drawColor(paint.getColor()) def drawPoint(self, x: float, y: float, paint: Paint): if HAS_PIL and self._draw is not None: c = paint.getColor() self._draw.point([(x, y)], fill=( Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c) )) def drawPoints(self, pts: List[float], offset: int = 0, count: int = -1, paint: Paint = None): if paint is None: paint = Paint() if count < 0: count = len(pts) - offset for i in range(offset, offset + count, 2): self.drawPoint(pts[i], pts[i + 1], paint) def drawLine(self, startX: float, startY: float, stopX: float, stopY: float, paint: Paint): if HAS_PIL and self._draw is not None: c = paint.getColor() w = paint.getStrokeWidth() if paint.getStyle() != Paint.Style.FILL else 1 self._draw.line([(startX, startY), (stopX, stopY)], fill=(Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)), width=max(1, int(w))) def drawLines(self, pts: List[float], offset: int = 0, count: int = -1, paint: Paint = None): if paint is None: paint = Paint() if count < 0: count = len(pts) - offset for i in range(offset, offset + count, 4) if count >= 4 else []: self.drawLine(pts[i], pts[i + 1], pts[i + 2], pts[i + 3], paint) def drawRect(self, rect: Union[Rect, RectF, tuple], paint: Paint): if isinstance(rect, Rect): l, t, r, b = rect.left, rect.top, rect.right, rect.bottom elif isinstance(rect, RectF): l, t, r, b = rect.left, rect.top, rect.right, rect.bottom else: l, t, r, b = rect if HAS_PIL and self._draw is not None: c = paint.getColor() fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) if paint.getStyle() == Paint.Style.FILL: self._draw.rectangle([l, t, r, b], fill=fill_color) elif paint.getStyle() == Paint.Style.STROKE: w = max(1, int(paint.getStrokeWidth())) self._draw.rectangle([l, t, r, b], outline=fill_color, width=w) else: self._draw.rectangle([l, t, r, b], fill=fill_color) def drawRoundRect(self, rect: Union[RectF, tuple], rx: float, ry: float, paint: Paint): if isinstance(rect, RectF): l, t, r, b = rect.left, rect.top, rect.right, rect.bottom else: l, t, r, b = rect if HAS_PIL and self._draw is not None: c = paint.getColor() fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) self._draw.rounded_rectangle( [l, t, r, b], radius=rx, fill=fill_color if paint.getStyle() != Paint.Style.STROKE else None, outline=fill_color if paint.getStyle() != Paint.Style.FILL else None, width=max(1, int(paint.getStrokeWidth())) if paint.getStyle() != Paint.Style.FILL else 1 ) def drawCircle(self, cx: float, cy: float, radius: float, paint: Paint): if HAS_PIL and self._draw is not None: c = paint.getColor() fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) bbox = [cx - radius, cy - radius, cx + radius, cy + radius] if paint.getStyle() == Paint.Style.FILL: self._draw.ellipse(bbox, fill=fill_color) elif paint.getStyle() == Paint.Style.STROKE: w = max(1, int(paint.getStrokeWidth())) self._draw.ellipse(bbox, outline=fill_color, width=w) else: self._draw.ellipse(bbox, fill=fill_color) def drawOval(self, rect: Union[RectF, tuple], paint: Paint): if isinstance(rect, RectF): l, t, r, b = rect.left, rect.top, rect.right, rect.bottom else: l, t, r, b = rect if HAS_PIL and self._draw is not None: c = paint.getColor() fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) self._draw.ellipse([l, t, r, b], fill=fill_color) def drawArc(self, rect: Union[RectF, tuple], startAngle: float, sweepAngle: float, useCenter: bool, paint: Paint): if isinstance(rect, RectF): l, t, r, b = rect.left, rect.top, rect.right, rect.bottom else: l, t, r, b = rect if HAS_PIL and self._draw is not None: c = paint.getColor() fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) if not HAS_PIL: return from PIL import ImageDraw self._draw.pieslice( [l, t, r, b], start=startAngle, end=startAngle + sweepAngle, fill=fill_color if useCenter or paint.getStyle() != Paint.Style.STROKE else None, outline=fill_color if paint.getStyle() != Paint.Style.FILL else None, width=max(1, int(paint.getStrokeWidth())) if paint.getStyle() != Paint.Style.FILL else 1 ) def drawPath(self, path: Path, paint: Paint): if HAS_PIL and self._draw is not None: pil_path = [] for seg in path._segments: if seg["type"] == "move": pil_path.append(("M", seg["x"], seg["y"])) elif seg["type"] == "line": pil_path.append(("L", seg["x"], seg["y"])) elif seg["type"] == "close": pil_path.append(("Z",)) elif seg["type"] in ("quad", "cubic"): pass if pil_path: from PIL import ImageDraw c = paint.getColor() fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) try: if paint.getStyle() == Paint.Style.FILL: self._draw.polygon( [(s[1], s[2]) for s in pil_path if s[0] in ("M", "L")], fill=fill_color ) else: w = max(1, int(paint.getStrokeWidth())) if len(pil_path) > 1: pts = [(s[1], s[2]) for s in pil_path if s[0] in ("M", "L")] for i in range(len(pts) - 1): self._draw.line([pts[i], pts[i + 1]], fill=fill_color, width=w) except Exception: pass def drawBitmap(self, bitmap: Bitmap, left: float, top: float, paint: Paint = None): if HAS_PIL and self._draw is not None and hasattr(bitmap, "_image") and bitmap._image: self._image.paste(bitmap._image, (int(left), int(top))) elif HAS_PIL and self._image is not None: pass def drawBitmap(self, bitmap: Bitmap, src: Rect, dst: RectF, paint: Paint = None): if HAS_PIL and self._image is not None and hasattr(bitmap, "_image") and bitmap._image: if src: cropped = bitmap._image.crop((src.left, src.top, src.right, src.bottom)) resized = cropped.resize((int(dst.width()), int(dst.height()))) self._image.paste(resized, (int(dst.left), int(dst.top))) else: resized = bitmap._image.resize((int(dst.width()), int(dst.height()))) self._image.paste(resized, (int(dst.left), int(dst.top))) def drawText(self, text: str, x: float, y: float, paint: Paint): if HAS_PIL and self._draw is not None: c = paint.getColor() fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) self._draw.text((x, y), text, fill=fill_color) def drawTextOnPath(self, text: str, path: Path, hOffset: float, vOffset: float, paint: Paint): pass def drawPosText(self, text: str, pos: List[float], paint: Paint): if HAS_PIL and self._draw is not None: c = paint.getColor() fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) for i, ch in enumerate(text): if i * 2 + 1 < len(pos): self._draw.text((pos[i * 2], pos[i * 2 + 1]), ch, fill=fill_color) def drawPicture(self, picture: Picture, dst: Union[Rect, RectF] = None): pass def drawPatch(self, patch: NinePatch, dst: Rect, paint: Paint = None): pass def drawDoubleRoundRect(self, outer: RectF, outerRx: float, outerRy: float, inner: RectF, innerRx: float, innerRy: float, paint: Paint): pass def drawBitmapMesh(self, bitmap: Bitmap, meshWidth: int, meshHeight: int, verts: List[float], vertOffset: int, colors: List[int], colorOffset: int, paint: Paint = None): pass def drawVertices(self, mode: int, vertexCount: int, verts: List[float], vertOffset: int, texs: List[float], texOffset: int, colors: List[int], colorOffset: int, indices: List[int], indexOffset: int, indexCount: int, paint: Paint): pass def isHardwareAccelerated(self) -> bool: return False def isOpaque(self) -> bool: return False def setDensity(self, density: int): pass def getDensity(self) -> int: return 0 def toString(self) -> str: return f"Canvas({self._width}x{self._height})" # ═══════════════════════════════════════════════════════════════ # __all__ # ═══════════════════════════════════════════════════════════════ __all__ = [ # Core "Bitmap", "BitmapFactory", "Canvas", "Paint", "Color", "ColorFilter", "PorterDuffColorFilter", "LightingColorFilter", "ComposeColorFilter", "ColorMatrix", "ColorMatrixColorFilter", "Path", "PathMeasure", "Rect", "RectF", "Point", "PointF", "Typeface", "Matrix", # Shader hierarchy "Shader", "LinearGradient", "RadialGradient", "SweepGradient", "BitmapShader", "ComposeShader", # MaskFilter hierarchy "MaskFilter", "BlurMaskFilter", "EmbossMaskFilter", # PorterDuff "PorterDuff", "PorterDuffXfermode", "Xfermode", "AvoidXfermode", "PixelXorXfermode", # DrawFilter "DrawFilter", "PaintFlagsDrawFilter", # PathEffect hierarchy "PathEffect", "SumPathEffect", "CornerPathEffect", "DashPathEffect", "DiscretePathEffect", "PathDashPathEffect", "ComposePathEffect", # Misc "Camera", "SurfaceTexture", "Region", "RegionIterator", "NinePatch", "Picture", "PictureDrawable", "Rasterizer", "LayerRasterizer", # BlendMode "BlendMode", "BlendModeColorFilter", "ClipOp", ]