Cap workers at half the cores to stop host freezes
This commit is contained in:
1 parent
724449f3f0
commit
a08151337b
1 file changed
+28
-11
+28
-11
@@ -219,7 +219,6 @@ def render_bitmap(ly: dict, img_path: Path, cw: int, ch: int
|
|||||||
Image.LANCZOS)
|
Image.LANCZOS)
|
||||||
L, T = math.floor(l), math.floor(t)
|
L, T = math.floor(l), math.floor(t)
|
||||||
layer = layer.crop((ix0 - L, iy0 - T, ix1 - L, iy1 - T))
|
layer = layer.crop((ix0 - L, iy0 - T, ix1 - L, iy1 - T))
|
||||||
apply_opacity(layer, ly.get("alpha", 255))
|
|
||||||
return layer, (ix0, iy0)
|
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)
|
outline=255, width=thickness)
|
||||||
img.paste(stroke_img, (0, 0), ring)
|
img.paste(stroke_img, (0, 0), ring)
|
||||||
|
|
||||||
apply_opacity(img, ly.get("alpha", 255))
|
|
||||||
# shadow (ppl renders a soft offset shadow per layer)
|
# shadow (ppl renders a soft offset shadow per layer)
|
||||||
sd = float(ly.get("shadowDistance", "0") or 0)
|
sd = float(ly.get("shadowDistance", "0") or 0)
|
||||||
sb = float(ly.get("shadowBlur", "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
|
return adv * c
|
||||||
|
|
||||||
grad = parse_gradient(ly.get("textColor", ""))
|
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) ---
|
# --- measure natural text size (multi-line aware, per-char spacing) ---
|
||||||
lines = text.split("\n")
|
lines = text.split("\n")
|
||||||
@@ -428,7 +430,11 @@ def render_text(ly: dict, cw: int, ch: int) -> Optional[Tuple[Image.Image, Tuple
|
|||||||
return None
|
return None
|
||||||
img = img.crop((ix0 - left, iy0 - top, ix1 - left, iy1 - top))
|
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)
|
# soft shadow (used by rasterized output / previews)
|
||||||
sd = float(ly.get("shadowDistance", "0") or 0)
|
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)
|
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
|
# PSD writer
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
@@ -744,7 +760,7 @@ def build_psd(layers, width: int, height: int, out_path: str, workers: int,
|
|||||||
for layer in layers:
|
for layer in layers:
|
||||||
name, opacity, visible, (lx, ly), img = layer[:5]
|
name, opacity, visible, (lx, ly), img = layer[:5]
|
||||||
if img is not None and visible and opacity > 0:
|
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_rgb = None # keep the RGBA merged image for the 4-channel output
|
||||||
# Merged image data is written RAW with a single compression field and
|
# 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
|
# 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
|
# Main
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
def _safe_workers(workers: int) -> int:
|
def _safe_workers(workers: int) -> int:
|
||||||
"""Never use every CPU core: leave one for the system, otherwise the
|
"""Keep at least half the CPU cores free - on phones the RLE-encoding
|
||||||
RLE-encoding workers can stall the whole device."""
|
workers plus PIL's internal threading can otherwise stall the whole
|
||||||
available = max(1, (os.cpu_count() or 2) - 1)
|
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))
|
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:
|
for layer in rendered:
|
||||||
name, opacity, visible, (lx, ly), img = layer[:5]
|
name, opacity, visible, (lx, ly), img = layer[:5]
|
||||||
if img is not None and visible and opacity > 0:
|
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
|
# downscale for easy viewing
|
||||||
preview = canvas.convert("RGB")
|
preview = canvas.convert("RGB")
|
||||||
if preview.width > 2000:
|
if preview.width > 2000:
|
||||||
@@ -937,8 +954,8 @@ def main():
|
|||||||
ap.add_argument("-o", "--output", help="output .psd path")
|
ap.add_argument("-o", "--output", help="output .psd path")
|
||||||
ap.add_argument("--preview", help="also write a downscaled composite PNG")
|
ap.add_argument("--preview", help="also write a downscaled composite PNG")
|
||||||
ap.add_argument("-j", "--workers", type=int,
|
ap.add_argument("-j", "--workers", type=int,
|
||||||
default=max(1, (os.cpu_count() or 2) - 1),
|
default=max(1, (os.cpu_count() or 2) // 2),
|
||||||
help="parallel workers for RLE encoding (default: cores-1)")
|
help="parallel workers for RLE encoding (default: cores/2)")
|
||||||
ap.add_argument("--type-layer", action="store_true",
|
ap.add_argument("--type-layer", action="store_true",
|
||||||
help="convert text layers to editable TypeLayers (TySh) "
|
help="convert text layers to editable TypeLayers (TySh) "
|
||||||
"instead of rasterized pixels")
|
"instead of rasterized pixels")
|
||||||
|
|||||||
Reference in new issue
Block a user