""" Android Graphics 组件 — Bitmap, Canvas, Paint, Color, Path, Rect 等 方法签名完全对齐 Android SDK """ import array import math import struct import io import os import time 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: """对两个路径执行布尔运算。基于包围盒的近似实现。""" if other is None: return False self_bounds = self.computeBounds() other_bounds = other.computeBounds() if op == Path.Op.UNION: # 合并:将 other 的段追加到 self self.addPath(other) return True if op == Path.Op.INTERSECT: # 交集:仅当包围盒相交时保留相交区域 if not RectF.intersects(self_bounds, other_bounds): self.reset() return False # 用相交包围盒作为近似结果 inter = RectF(self_bounds.left, self_bounds.top, self_bounds.right, self_bounds.bottom) inter.intersect(other_bounds.left, other_bounds.top, other_bounds.right, other_bounds.bottom) self.reset() self.addRect(inter.left, inter.top, inter.right, inter.bottom) return True if op == Path.Op.DIFFERENCE: # 差集:若包围盒不相交则 self 不变,否则标记(近似) if not RectF.intersects(self_bounds, other_bounds): return True # 近似:无法精确计算,保留 self 不变 return True if op == Path.Op.REVERSE_DIFFERENCE: # 反向差集:用 other 替换 self self.set(other) return True if op == Path.Op.XOR: # 异或:合并两者 self.addPath(other) return True 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 self._contours: List[List[Tuple[float, float, float]]] = [] self._contour_lengths: List[float] = [] self._total_length: float = 0.0 if path is not None: self._flatten() def _flatten(self): """将路径展平为线段列表,计算累积长度。""" self._contours = [] self._contour_lengths = [] self._total_length = 0.0 if not self._path: return segs = self._path._segments cur_x = 0.0 cur_y = 0.0 start_x = 0.0 start_y = 0.0 contour: List[Tuple[float, float, float]] = [] contour_len = 0.0 for seg in segs: t = seg.get("type") if t == "move": if contour: self._contours.append(contour) self._contour_lengths.append(contour_len) self._total_length += contour_len cur_x = seg.get("x", 0.0) cur_y = seg.get("y", 0.0) start_x = cur_x start_y = cur_y contour = [(cur_x, cur_y, 0.0)] contour_len = 0.0 elif t == "line": nx = seg.get("x", cur_x) ny = seg.get("y", cur_y) d = math.hypot(nx - cur_x, ny - cur_y) contour_len += d contour.append((nx, ny, contour_len)) cur_x = nx cur_y = ny elif t == "quad": x1, y1 = seg.get("x1", cur_x), seg.get("y1", cur_y) x2, y2 = seg.get("x2", cur_x), seg.get("y2", cur_y) steps = 16 prev_x, prev_y = cur_x, cur_y for i in range(1, steps + 1): tt = i / steps mt = 1 - tt nx = mt * mt * cur_x + 2 * mt * tt * x1 + tt * tt * x2 ny = mt * mt * cur_y + 2 * mt * tt * y1 + tt * tt * y2 d = math.hypot(nx - prev_x, ny - prev_y) contour_len += d contour.append((nx, ny, contour_len)) prev_x, prev_y = nx, ny cur_x = x2 cur_y = y2 elif t == "cubic": x1, y1 = seg.get("x1", cur_x), seg.get("y1", cur_y) x2, y2 = seg.get("x2", cur_x), seg.get("y2", cur_y) x3, y3 = seg.get("x3", cur_x), seg.get("y3", cur_y) steps = 24 prev_x, prev_y = cur_x, cur_y for i in range(1, steps + 1): tt = i / steps mt = 1 - tt nx = mt ** 3 * cur_x + 3 * mt ** 2 * tt * x1 + 3 * mt * tt ** 2 * x2 + tt ** 3 * x3 ny = mt ** 3 * cur_y + 3 * mt ** 2 * tt * y1 + 3 * mt * tt ** 2 * y2 + tt ** 3 * y3 d = math.hypot(nx - prev_x, ny - prev_y) contour_len += d contour.append((nx, ny, contour_len)) prev_x, prev_y = nx, ny cur_x = x3 cur_y = y3 elif t == "close": if contour: d = math.hypot(start_x - cur_x, start_y - cur_y) if d > 1e-10: contour_len += d contour.append((start_x, start_y, contour_len)) cur_x = start_x cur_y = start_y if contour: if self._force_closed and segs and segs[-1].get("type") != "close": d = math.hypot(start_x - cur_x, start_y - cur_y) if d > 1e-10: contour_len += d contour.append((start_x, start_y, contour_len)) self._contours.append(contour) self._contour_lengths.append(contour_len) self._total_length += contour_len def setPath(self, path: Path, forceClosed: bool): self._path = path self._force_closed = forceClosed self._contours = [] self._contour_lengths = [] self._total_length = 0.0 if path is not None: self._flatten() def getLength(self) -> float: """返回当前轮廓的长度。""" if self._contour_lengths: return self._contour_lengths[0] return 0.0 def getPosTan(self, distance: float, pos: List[float], tan: List[float]) -> bool: """获取指定距离处的坐标和切线方向。""" if not self._contours: return False contour = self._contours[0] length = self._contour_lengths[0] if length <= 0: return False # 钳制距离 d = max(0.0, min(distance, length)) # 二分查找 lo, hi = 0, len(contour) - 1 while lo < hi: mid = (lo + hi) // 2 if contour[mid][2] < d: lo = mid + 1 else: hi = mid idx = max(1, lo) px, py, pd = contour[idx - 1] nx, ny, nd = contour[idx] seg_len = nd - pd if seg_len < 1e-10: t = 0.0 else: t = (d - pd) / seg_len x = px + (nx - px) * t y = py + (ny - py) * t dx = nx - px dy = ny - py dl = math.hypot(dx, dy) if dl < 1e-10: tx, ty = 1.0, 0.0 else: tx, ty = dx / dl, dy / dl if len(pos) >= 2: pos[0] = x pos[1] = y if len(tan) >= 2: tan[0] = tx tan[1] = ty return True def getMatrix(self, distance: float, matrix: "Matrix", flags: int) -> bool: """获取指定距离处的变换矩阵(平移+旋转)。""" pos = [0.0, 0.0] tan = [0.0, 0.0] if not self.getPosTan(distance, pos, tan): return False angle = math.degrees(math.atan2(tan[1], tan[0])) matrix.reset() matrix.postRotate(angle) matrix.postTranslate(pos[0], pos[1]) return True def getSegment(self, startD: float, stopD: float, dst: Path, startWithMoveTo: bool) -> bool: """提取路径中 [startD, stopD] 区间的子路径。""" if not self._contours: return False contour = self._contours[0] length = self._contour_lengths[0] if length <= 0: return False startD = max(0.0, min(startD, length)) stopD = max(0.0, min(stopD, length)) if startD >= stopD: return False first = True for i in range(1, len(contour)): px, py, pd = contour[i - 1] nx, ny, nd = contour[i] if nd < startD: continue if pd > stopD: break seg_len = nd - pd if seg_len < 1e-10: continue # 计算线段两端在 [startD, stopD] 内的参数 t0 = max(0.0, (startD - pd) / seg_len) t1 = min(1.0, (stopD - pd) / seg_len) x0 = px + (nx - px) * t0 y0 = py + (ny - py) * t0 x1 = px + (nx - px) * t1 y1 = py + (ny - py) * t1 if first and startWithMoveTo: dst.moveTo(x0, y0) else: dst.lineTo(x0, y0) dst.lineTo(x1, y1) first = False return True def isClosed(self) -> bool: if not self._path or not self._path._segments: return self._force_closed if self._force_closed: return True return any(s.get("type") == "close" for s in self._path._segments) def nextContour(self) -> bool: """移动到下一个轮廓。""" if len(self._contours) > 1: self._contours.pop(0) self._contour_lengths.pop(0) return True 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 self._rot_x = 0.0 self._rot_y = 0.0 self._rot_z = 0.0 self._trans_x = 0.0 self._trans_y = 0.0 self._trans_z = 0.0 self._stack: List[Tuple[float, float, float, float, float, float, float, float, float]] = [] def save(self): """保存当前变换状态到栈。""" self._stack.append((self._rot_x, self._rot_y, self._rot_z, self._trans_x, self._trans_y, self._trans_z, self._location_x, self._location_y, self._location_z)) def restore(self): """从栈恢复上次保存的变换状态。""" if self._stack: (self._rot_x, self._rot_y, self._rot_z, self._trans_x, self._trans_y, self._trans_z, self._location_x, self._location_y, self._location_z) = self._stack.pop() def translate(self, x: float, y: float, z: float): self._trans_x += x self._trans_y += y self._trans_z += z def rotateX(self, deg: float): """绕 X 轴旋转(累积角度)。""" self._rot_x += deg def rotateY(self, deg: float): """绕 Y 轴旋转(累积角度)。""" self._rot_y += deg def rotateZ(self, deg: float): """绕 Z 轴旋转(累积角度)。""" self._rot_z += deg def rotate(self, x: float, y: float, z: float): """同时绕 X/Y/Z 轴旋转。""" self._rot_x += x self._rot_y += y self._rot_z += z def _compute_matrix(self) -> List[float]: """计算 4x4 变换矩阵并投影为 3x3 仿射矩阵。""" rx = math.radians(self._rot_x) ry = math.radians(self._rot_y) rz = math.radians(self._rot_z) cx, sx = math.cos(rx), math.sin(rx) cy, sy = math.cos(ry), math.sin(ry) cz, sz = math.cos(rz), math.sin(rz) # 旋转矩阵 R = Rz * Ry * Rx r00 = cz * cy r01 = cz * sy * sx - sz * cx r02 = cz * sy * cx + sz * sx r10 = sz * cy r11 = sz * sy * sx + cz * cx r12 = sz * sy * cx - cz * sx r20 = -sy r21 = cy * sx r22 = cy * cx # 应用平移 tx = self._trans_x ty = self._trans_y tz = self._trans_z # 相机位置(z 越负越远) cam_z = self._location_z # 透视投影:z' = z + cam_z,当 cam_z != 0 时做透视除法 # 简化为:将 3D 点投影到 2D # 点 (x,y,z) -> (x*r00+y*r01+z*r02+tx, x*r10+y*r11+z*r12+ty, x*r20+y*r21+z*r22+tz) # 透视:screen_x = x' * d / (d - z'), screen_y = y' * d / (d - z') d = -cam_z # 相机到原点距离 if abs(d) < 1e-10: # 正交投影 return [r00, r01, tx, r10, r11, ty, 0.0, 0.0, 1.0] # 透视投影矩阵(近似):将 z 影响缩放到 x,y # m[0]=r00, m[1]=r01, m[2]=tx, m[3]=r10, m[4]=r11, m[5]=ty # 透视项:m[6]=r20/d, m[7]=r21/d, m[8]=1 - tz/d return [r00, r01, tx, r10, r11, ty, r20 / d, r21 / d, 1.0 - tz / d] def getMatrix(self, matrix: Matrix): """计算变换矩阵并写入给定的 Matrix。""" matrix._values = self._compute_matrix() def applyToCanvas(self, canvas: "Canvas"): """将相机变换应用到 Canvas。""" m = Matrix() self.getMatrix(m) if hasattr(canvas, "concat"): canvas.concat(m) 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: """计算向量与变换后 Z 轴法线的点积。""" rx = math.radians(self._rot_x) ry = math.radians(self._rot_y) rz = math.radians(self._rot_z) cy, sy = math.cos(ry), math.sin(ry) cx, sx = math.cos(rx), math.sin(rx) # 变换后的 Z 轴法线 nz_x = sy nz_y = -cy * sx nz_z = cy * cx return dx * nz_x + dy * nz_y + dz * nz_z # ═══════════════════════════════════════════════════════════════ # 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"): """回放录制的绘制命令到指定 Canvas。""" for cmd in self._recorded: method = cmd.get("method") args = cmd.get("args", []) if method and hasattr(canvas, method): try: getattr(canvas, method)(*args) except Exception: 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"): """将 Picture 回放到指定 Canvas。""" if self._picture is not None: self._picture.draw(canvas) # ═══════════════════════════════════════════════════════════════ # SurfaceTexture # ═══════════════════════════════════════════════════════════════ class SurfaceTexture(Stub): """SurfaceTexture — 表面纹理""" def __init__(self, texName: int = 0): super().__init__() self._tex_name = texName self._timestamp = 0 self._buffer_width = 0 self._buffer_height = 0 self._attached = texName != 0 self._released = False def getTimestamp(self) -> int: return self._timestamp def updateTexImage(self): self._timestamp = int(time.time() * 1e9) def release(self): """释放 SurfaceTexture 资源。""" self._released = True self._attached = False def setDefaultBufferSize(self, width: int, height: int): """设置默认缓冲区大小。""" self._buffer_width = width self._buffer_height = height def attachToGLContext(self, texName: int): """将 SurfaceTexture 附加到 GL 纹理。""" self._tex_name = texName self._attached = True self._released = False def detachFromGLContext(self): """从 GL 纹理分离 SurfaceTexture。""" self._attached = False # ═══════════════════════════════════════════════════════════════ # 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: """对区域执行布尔运算。支持 op(rect, op) / op(region, op) / op(left,top,right,bottom,op)。""" if len(args) == 2: operand, op_code = args if isinstance(operand, Rect): other = Rect(operand.left, operand.top, operand.right, operand.bottom) elif isinstance(operand, Region): other = Rect(operand._rect.left, operand._rect.top, operand._rect.right, operand._rect.bottom) else: return False elif len(args) == 5: left, top, right, bottom, op_code = args other = Rect(left, top, right, bottom) else: return False self_r = self._rect if op_code == Region.OP_INTERSECT: nL = max(self_r.left, other.left) nT = max(self_r.top, other.top) nR = min(self_r.right, other.right) nB = min(self_r.bottom, other.bottom) if nL < nR and nT < nB: self_r.set(nL, nT, nR, nB) return True self_r.setEmpty() return False if op_code == Region.OP_UNION: if self_r.isEmpty(): self_r.set(other.left, other.top, other.right, other.bottom) else: self_r.left = min(self_r.left, other.left) self_r.top = min(self_r.top, other.top) self_r.right = max(self_r.right, other.right) self_r.bottom = max(self_r.bottom, other.bottom) return True if op_code == Region.OP_REPLACE: self_r.set(other.left, other.top, other.right, other.bottom) return True if op_code == Region.OP_DIFFERENCE: # 差集:若不相交则不变;若完全包含则清空;否则近似为不变 if not Rect.intersects(self_r, other): return True if other.contains(self_r.left, self_r.top) and other.contains(self_r.right - 1, self_r.bottom - 1): self_r.setEmpty() return False return True if op_code == Region.OP_REVERSE_DIFFERENCE: self_r.set(other.left, other.top, other.right, other.bottom) return True if op_code == Region.OP_XOR: if not Rect.intersects(self_r, other): self_r.union(other.left, other.top, other.right, other.bottom) return True 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 self._font_cache = None def getTextSize(self) -> float: return self._text_size def _get_font(self): """获取 PIL ImageFont 对象(带缓存)。""" if not HAS_PIL: return None cache_key = (self._text_size, id(self._typeface) if self._typeface else None) if getattr(self, "_font_cache_key", None) == cache_key and getattr(self, "_font_cache", None): return self._font_cache try: from PIL import ImageFont font_path = None if self._typeface and hasattr(self._typeface, "_path") and self._typeface._path: font_path = self._typeface._path if font_path and os.path.exists(font_path): self._font_cache = ImageFont.truetype(font_path, int(self._text_size)) else: self._font_cache = ImageFont.load_default() self._font_cache_key = cache_key return self._font_cache except Exception: return None 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: """测量文本宽度。优先使用 PIL ImageFont 获取精确宽度。""" font = self._get_font() if font is not None: try: if hasattr(font, "getlength"): return float(font.getlength(text)) bbox = font.getbbox(text) return float(bbox[2] - bbox[0]) except Exception: pass return len(text) * self._text_size * 0.5 def breakText(self, text: str, maxWidth: float, measuredWidth: List[float]) -> int: """在最大宽度内截断文本,返回可容纳的字符数。""" font = self._get_font() width = 0.0 count = 0 for i, ch in enumerate(text): if font is not None: try: cw = font.getlength(ch) if hasattr(font, "getlength") else (font.getbbox(ch)[2]) except Exception: cw = self._text_size * 0.5 else: 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: """获取每个字符的宽度。""" font = self._get_font() for i, ch in enumerate(text): if font is not None: try: widths[i] = float(font.getlength(ch)) if hasattr(font, "getlength") else float(font.getbbox(ch)[2]) except Exception: widths[i] = self._text_size * 0.5 else: widths[i] = self._text_size * 0.5 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]: """返回字体度量信息。优先使用 PIL ImageFont 获取真实度量。""" size = self._text_size font = self._get_font() if font is not None: try: if hasattr(font, "getmetrics"): ascent, descent = font.getmetrics() return { "ascent": -float(ascent), "descent": float(descent), "top": -float(ascent) - 2.0, "bottom": float(descent) + 2.0, "leading": 0.0, } except Exception: pass 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 def setStrokeCap(self, cap): self._stroke_cap = cap def getStrokeCap(self): return getattr(self, '_stroke_cap', 0) def setStrokeJoin(self, join): self._stroke_join = join def getStrokeJoin(self): return getattr(self, '_stroke_join', 0) def setStrokeMiter(self, miter): self._stroke_miter = miter def getStrokeMiter(self): return getattr(self, '_stroke_miter', 4.0) def getTextBounds(self, text, start, end, bounds): """返回文本边界矩形。优先使用 PIL ImageFont 获取真实边界。""" font = self._get_font() if font is not None: try: sub = text[start:end] bbox = font.getbbox(sub) bounds.left = int(bbox[0]) bounds.top = int(bbox[1]) bounds.right = int(bbox[2]) bounds.bottom = int(bbox[3]) return except Exception: pass if hasattr(self, '_text_size'): w = len(text[start:end]) * self._text_size * 0.5 h = self._text_size else: w = len(text[start:end]) * 12 h = 24 bounds.left = 0 bounds.top = -int(h * 0.8) bounds.right = int(w) bounds.bottom = int(h * 0.2) def setBlendMode(self, blendmode): self._blend_mode = blendmode def getBlendMode(self): return getattr(self, '_blend_mode', None) def setLetterSpacing(self, letterSpacing): self._letter_spacing = letterSpacing def getLetterSpacing(self): return getattr(self, '_letter_spacing', 0.0) def setFontFeatureSettings(self, settings): self._font_feature_settings = settings def getFontFeatureSettings(self): return getattr(self, '_font_feature_settings', None) def setHinting(self, mode): self._hinting = mode def getHinting(self): return getattr(self, '_hinting', 2) # HINTING_ON def setFakeBoldText(self, fakeBoldText): self._fake_bold_text = fakeBoldText def isFakeBoldText(self): return getattr(self, '_fake_bold_text', False) def hasGlyph(self, string): return len(string) > 0 # ═══════════════════════════════════════════════════════════════ # 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: """比较两个 Bitmap 的像素是否完全相同。""" if not isinstance(other, Bitmap): return False if self._width != other._width or self._height != other._height: return False if self._config != other._config: return False if self._recycled or other._recycled: return False # 使用 PIL 比较像素 if HAS_PIL and hasattr(self, "_image") and hasattr(other, "_image"): try: from PIL import ImageChops diff = ImageChops.difference(self._image, other._image) return diff.getbbox() is None except Exception: return list(self._image.getdata()) == list(other._image.getdata()) # 回退到字节比较 if hasattr(self, "_pixels") and hasattr(other, "_pixels"): return bytes(self._pixels) == bytes(other._pixels) return True def prepareToDraw(self): """提示将位图数据上传到 GPU 缓存。桌面端为空操作。""" 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]: """从资源解码位图。尝试通过 Resources 打开原始资源流。""" # 尝试通过 Resources 获取资源路径或流 if res is not None: try: # 尝试 openRawResource stream = res.openRawResource(resId) if stream is not None: return BitmapFactory.decodeStream(stream, None, opts) except Exception: pass try: # 尝试通过资源名查找文件 name = res.getResourceEntryName(resId) if hasattr(res, "getResourceEntryName") else None if name and name != "unknown": for ext in (".png", ".jpg", ".jpeg", ".webp", ".bmp"): candidate = resolve_android_path("res/drawable/" + name + ext) if os.path.exists(candidate): return BitmapFactory.decodeFile(candidate, opts) candidate = resolve_android_path("res/drawable-xxhdpi/" + name + ext) if os.path.exists(candidate): return BitmapFactory.decodeFile(candidate, opts) except Exception: pass 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=0, height=0): super().__init__() # 支持 Canvas(Bitmap) 重载 if isinstance(width, Bitmap): bitmap = width self._bitmap = bitmap self._width = bitmap.getWidth() self._height = bitmap.getHeight() if HAS_PIL: from PIL import Image, ImageDraw self._image = bitmap._image if hasattr(bitmap, '_image') and bitmap._image else None self._draw = ImageDraw.Draw(self._image) if self._image else None else: self._pixels = None self._save_count = 0 self._matrix_stack = [Matrix()] self._clip_stack = [Rect(0, 0, self._width, self._height)] self._matrix = Matrix() return 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: """判断给定矩形是否完全在裁剪区域之外。""" clip = self._clip_stack[-1] if len(args) == 1: r = args[0] if isinstance(r, Rect): left, top, right, bottom = r.left, r.top, r.right, r.bottom elif isinstance(r, RectF): left, top, right, bottom = r.left, r.top, r.right, r.bottom else: return False elif len(args) == 4: left, top, right, bottom = args else: return False return (right <= clip.left or left >= clip.right or bottom <= clip.top or top >= clip.bottom) 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, *args): """drawBitmap 重载: (bitmap,left,top,paint) / (bitmap,src,dst,paint) / (bitmap,matrix,paint)""" if len(args) == 4: bitmap, a, b, paint = args if isinstance(a, (int, float)) and isinstance(b, (int, float)): if HAS_PIL and self._draw is not None and hasattr(bitmap, "_image") and bitmap._image: self._image.paste(bitmap._image, (int(a), int(b))) elif HAS_PIL and self._image is not None: pass else: src, dst = a, b 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))) elif len(args) == 3: bitmap, matrix, paint = args if HAS_PIL and self._image is not None and hasattr(bitmap, "_image") and bitmap._image: try: from PIL import Image inv = Matrix() if hasattr(matrix, "invert") and matrix.invert(inv): v = inv.getValues() elif hasattr(matrix, "getValues"): v = matrix.getValues() else: v = [1, 0, 0, 0, 1, 0, 0, 0, 1] transformed = bitmap._image.transform( bitmap._image.size, Image.AFFINE, (v[0], v[1], v[2], v[3], v[4], v[5])) self._image.paste(transformed, (0, 0)) except Exception: pass 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): """沿路径绘制文本。使用 PathMeasure 逐字符放置。""" if not text or not HAS_PIL or self._draw is None: return pm = PathMeasure(path, False) total = pm.getLength() if total <= 0: return c = paint.getColor() fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) dist = hOffset pos = [0.0, 0.0] tan = [0.0, 0.0] for ch in text: cw = paint.measureText(ch) if dist > total: break if pm.getPosTan(dist, pos, tan): angle = math.degrees(math.atan2(tan[1], tan[0])) # 使用 PIL 旋转绘制每个字符 try: from PIL import Image as _PILImage char_img = _PILImage.new("RGBA", (int(cw) + 4, int(paint._text_size) + 4), (0, 0, 0, 0)) from PIL import ImageDraw as _PILDraw cd = _PILDraw.Draw(char_img) cd.text((0, 0), ch, fill=fill_color) rotated = char_img.rotate(-angle, expand=True, resample=_PILImage.BICUBIC) self._image.paste(rotated, (int(pos[0] + vOffset * tan[1]), int(pos[1] - vOffset * tan[0])), rotated) except Exception: self._draw.text((pos[0], pos[1]), ch, fill=fill_color) dist += cw 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): """回放 Picture 中录制的绘制命令。""" if picture is None: return recorded = getattr(picture, "_recorded", []) if not recorded: return for cmd in recorded: method = cmd.get("method") args = cmd.get("args", []) if method and hasattr(self, method): try: getattr(self, method)(*args) except Exception: pass def drawPatch(self, patch: NinePatch, dst: Rect, paint: Paint = None): """绘制 NinePatch(拉伸位图)到目标矩形。""" if patch is None or patch._bitmap is None: return bitmap = patch._bitmap if dst is None: self.drawBitmap(bitmap, 0, 0, paint) return # 简单缩放绘制 if isinstance(dst, (Rect, RectF)): dw = dst.right - dst.left dh = dst.bottom - dst.top else: dw, dh = dst[2] - dst[0], dst[3] - dst[1] scaled = Bitmap.createScaledBitmap(bitmap, int(dw), int(dh), True) self.drawBitmap(scaled, dst.left if hasattr(dst, "left") else dst[0], dst.top if hasattr(dst, "top") else dst[1], paint) def drawDoubleRoundRect(self, outer: RectF, outerRx: float, outerRy: float, inner: RectF, innerRx: float, innerRy: float, paint: Paint): """绘制双层圆角矩形(外层和内层)。""" self.drawRoundRect(outer, outerRx, outerRy, paint) self.drawRoundRect(inner, innerRx, innerRy, paint) 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): """设置画布密度(DPI)。""" self._density = density def getDensity(self) -> int: """返回画布密度(DPI),默认 0 表示未知。""" return getattr(self, "_density", 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", ]