Cap workers at half the cores to stop host freezes

This commit is contained in:
AskaEth committed 2026-08-28 10:05:52 +08:00
1 parent 724449f3f0
commit a08151337b
1 file changed
+28 -11
+28 -11
View File
@@ -219,7 +219,6 @@ def render_bitmap(ly: dict, img_path: Path, cw: int, ch: int
Image.LANCZOS)
L, T = math.floor(l), math.floor(t)
layer = layer.crop((ix0 - L, iy0 - T, ix1 - L, iy1 - T))
apply_opacity(layer, ly.get("alpha", 255))
return layer, (ix0, iy0)
@@ -269,7 +268,6 @@ def render_shape(ly: dict, cw: int, ch: int) -> Optional[Tuple[Image.Image, Tupl
outline=255, width=thickness)
img.paste(stroke_img, (0, 0), ring)
apply_opacity(img, ly.get("alpha", 255))
# shadow (ppl renders a soft offset shadow per layer)
sd = float(ly.get("shadowDistance", "0") or 0)
sb = float(ly.get("shadowBlur", "0") or 0)
@@ -352,7 +350,11 @@ def render_text(ly: dict, cw: int, ch: int) -> Optional[Tuple[Image.Image, Tuple
return adv * c
grad = parse_gradient(ly.get("textColor", ""))
fill = grad["colors"][0][1] if grad["colors"] else (0, 0, 0, 255)
if grad["type"] == "gradient":
fill = (255, 255, 255, 255) # base color; gradient is applied after
grad = grad # keep for the mask pass
else:
fill = grad["colors"][0][1] if grad["colors"] else (0, 0, 0, 255)
# --- measure natural text size (multi-line aware, per-char spacing) ---
lines = text.split("\n")
@@ -428,7 +430,11 @@ def render_text(ly: dict, cw: int, ch: int) -> Optional[Tuple[Image.Image, Tuple
return None
img = img.crop((ix0 - left, iy0 - top, ix1 - left, iy1 - top))
apply_opacity(img, ly.get("alpha", 255))
# text gradient fill: paint a gradient over the glyph alpha
if grad["type"] == "gradient":
gimg = make_linear_gradient(img.width, img.height, grad)
img.paste(gimg, (0, 0), img.getchannel("A"))
# soft shadow (used by rasterized output / previews)
sd = float(ly.get("shadowDistance", "0") or 0)
@@ -453,6 +459,16 @@ def apply_opacity(img: Image.Image, alpha: str) -> None:
img.putalpha(alpha_ch)
def _opacity_paste(canvas: Image.Image, img: Image.Image, pos, opacity: int) -> None:
"""Composite a layer onto the merged/preview canvas, applying the layer
opacity (PSD opacity field is applied by Photoshop itself, but the
in-tool merged preview must apply it manually)."""
if opacity < 255:
img = img.copy()
img.putalpha(img.getchannel("A").point(lambda p: p * opacity // 255))
canvas.paste(img, pos, img)
# --------------------------------------------------------------------------
# PSD writer
# --------------------------------------------------------------------------
@@ -744,7 +760,7 @@ def build_psd(layers, width: int, height: int, out_path: str, workers: int,
for layer in layers:
name, opacity, visible, (lx, ly), img = layer[:5]
if img is not None and visible and opacity > 0:
merged.paste(img, (lx, ly), img)
_opacity_paste(merged, img, (lx, ly), opacity)
merged_rgb = None # keep the RGBA merged image for the 4-channel output
# Merged image data is written RAW with a single compression field and
# all planes in planar order (R G B A). 4 channels because the document
@@ -784,9 +800,10 @@ def build_psd(layers, width: int, height: int, out_path: str, workers: int,
# Main
# --------------------------------------------------------------------------
def _safe_workers(workers: int) -> int:
"""Never use every CPU core: leave one for the system, otherwise the
RLE-encoding workers can stall the whole device."""
available = max(1, (os.cpu_count() or 2) - 1)
"""Keep at least half the CPU cores free - on phones the RLE-encoding
workers plus PIL's internal threading can otherwise stall the whole
device (a previous cores-1 cap still froze the host)."""
available = max(1, (os.cpu_count() or 2) // 2)
return max(1, min(workers, available))
@@ -919,7 +936,7 @@ def convert(ppl_path: str, out_path: str, preview_path: Optional[str] = None,
for layer in rendered:
name, opacity, visible, (lx, ly), img = layer[:5]
if img is not None and visible and opacity > 0:
canvas.paste(img, (lx, ly), img)
_opacity_paste(canvas, img, (lx, ly), opacity)
# downscale for easy viewing
preview = canvas.convert("RGB")
if preview.width > 2000:
@@ -937,8 +954,8 @@ def main():
ap.add_argument("-o", "--output", help="output .psd path")
ap.add_argument("--preview", help="also write a downscaled composite PNG")
ap.add_argument("-j", "--workers", type=int,
default=max(1, (os.cpu_count() or 2) - 1),
help="parallel workers for RLE encoding (default: cores-1)")
default=max(1, (os.cpu_count() or 2) // 2),
help="parallel workers for RLE encoding (default: cores/2)")
ap.add_argument("--type-layer", action="store_true",
help="convert text layers to editable TypeLayers (TySh) "
"instead of rasterized pixels")