Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions backend/api/cl2k_maker.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ class GenerateRequest(BaseModel):
# fit (sides crop) so a wide backdrop isn't shrunk to a tiny strip; in cover
# ("Fill"), <1 shrinks the art below the fill onto black. 1.0 = plain fit/cover.
zoom: float = Field(1.0, ge=geo.ZOOM_MIN, le=geo.ZOOM_MAX)
# Mirror: flip the artwork horizontally. Applied at the END of framing, so
# the crop/focal point, the AI mask and the logo/label are unaffected.
mirror: bool = False
# Explicit bottom banner (e.g. "COMPLETE LIMITED SERIES"); overrides the auto
# COLLECTION / season label when set.
band_label: str = ""
Expand Down Expand Up @@ -661,6 +664,7 @@ def preview(
crop=_crop_tuple(req),
v_pos=req.v_pos,
zoom=req.zoom,
mirror=req.mirror,
band_label=req.band_label,
logo_scale=req.logo_scale,
logo_y_offset=req.logo_y_offset,
Expand Down Expand Up @@ -723,6 +727,7 @@ def generate(
crop=_crop_tuple(req),
v_pos=req.v_pos,
zoom=req.zoom,
mirror=req.mirror,
band_label=req.band_label,
logo_scale=req.logo_scale,
logo_y_offset=req.logo_y_offset,
Expand Down Expand Up @@ -779,6 +784,7 @@ class SquareArtRequest(BaseModel):
v_pos: float = Field(0.0, ge=geo.V_POS_MIN, le=geo.V_POS_MAX)
fit_mode: str = "cover" # cover (focal crop) | fit (contain on black)
zoom: float = Field(1.0, ge=geo.ZOOM_MIN, le=geo.ZOOM_MAX)
mirror: bool = False # flip the artwork horizontally
save_local: bool = True
upload_gdrive: Optional[bool] = None

Expand Down Expand Up @@ -867,6 +873,7 @@ def square_preview(
fit_mode=req.fit_mode,
v_pos=req.v_pos,
zoom=req.zoom,
mirror=req.mirror,
),
)

Expand Down Expand Up @@ -902,6 +909,7 @@ def square_generate(
fit_mode=req.fit_mode,
v_pos=req.v_pos,
zoom=req.zoom,
mirror=req.mirror,
season_number=req.season_number,
save_local=req.save_local,
upload_gdrive=req.upload_gdrive,
Expand All @@ -927,6 +935,7 @@ class BackgroundArtRequest(BaseModel):
v_pos: float = Field(0.0, ge=geo.V_POS_MIN, le=geo.V_POS_MAX)
fit_mode: str = "cover" # cover (focal crop) | fit (contain on black)
zoom: float = Field(1.0, ge=geo.ZOOM_MIN, le=geo.ZOOM_MAX)
mirror: bool = False # flip the artwork horizontally
resolution: str = "1080p" # 1080p (1920x1080) | 4k (3840x2160), per Plex dims
save_local: bool = True
upload_gdrive: Optional[bool] = None
Expand Down Expand Up @@ -954,6 +963,7 @@ def background_preview(
fit_mode=req.fit_mode,
v_pos=req.v_pos,
zoom=req.zoom,
mirror=req.mirror,
),
)

Expand Down Expand Up @@ -989,6 +999,7 @@ def background_generate(
fit_mode=req.fit_mode,
v_pos=req.v_pos,
zoom=req.zoom,
mirror=req.mirror,
resolution=req.resolution,
season_number=req.season_number,
save_local=req.save_local,
Expand Down Expand Up @@ -1176,6 +1187,7 @@ def psd_export(
crop=_crop_tuple(req),
v_pos=req.v_pos,
zoom=req.zoom,
mirror=req.mirror,
whiten=req.whiten,
flat_white=req.flat_white,
logo_3d=req.logo_3d,
Expand Down Expand Up @@ -1219,6 +1231,7 @@ class SeasonsRequest(BaseModel):
crop_h: Optional[float] = None
v_pos: float = Field(0.0, ge=geo.V_POS_MIN, le=geo.V_POS_MAX)
zoom: float = Field(1.0, ge=geo.ZOOM_MIN, le=geo.ZOOM_MAX)
mirror: bool = False # flip the artwork horizontally
logo_scale: float = Field(1.0, ge=geo.LOGO_SCALE_MIN, le=geo.LOGO_SCALE_MAX)
logo_y_offset: int = Field(0, ge=geo.LOGO_Y_OFFSET_MIN, le=geo.LOGO_Y_OFFSET_MAX)
whiten: Optional[bool] = None # None = module config (whiten_logo)
Expand Down Expand Up @@ -1374,6 +1387,7 @@ def _progress(entry: Dict[str, Any]) -> None:
crop=_crop_tuple(req),
v_pos=req.v_pos,
zoom=req.zoom,
mirror=req.mirror,
logo_scale=req.logo_scale,
logo_y_offset=req.logo_y_offset,
whiten=req.whiten,
Expand Down
12 changes: 12 additions & 0 deletions backend/modules/cl2k_maker.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ def _resolve_and_render(
crop: Optional[Tuple[float, float, float, float]] = None,
v_pos: float = 0.0,
zoom: float = 1.0,
mirror: bool = False,
band_label: str = "",
logo_scale: float = 1.0,
logo_y_offset: int = 0,
Expand Down Expand Up @@ -392,6 +393,7 @@ def _resolve_and_render(
crop=crop,
v_pos=v_pos,
zoom=zoom,
mirror=mirror,
band_label=band_label,
place_logo=place_logo,
text_logo_stroke=cfg.text_logo_stroke,
Expand Down Expand Up @@ -435,6 +437,7 @@ def generate_for_item(
crop: Optional[Tuple[float, float, float, float]] = None,
v_pos: float = 0.0,
zoom: float = 1.0,
mirror: bool = False,
band_label: str = "",
logo_scale: float = 1.0,
logo_y_offset: int = 0,
Expand Down Expand Up @@ -507,6 +510,7 @@ def generate_for_item(
crop=crop,
v_pos=v_pos,
zoom=zoom,
mirror=mirror,
band_label=band_label,
logo_scale=logo_scale,
logo_y_offset=logo_y_offset,
Expand Down Expand Up @@ -560,6 +564,7 @@ def generate_square_art(
fit_mode: str = "cover",
v_pos: float = 0.0,
zoom: float = 1.0,
mirror: bool = False,
season_number: Optional[int] = None,
save_local: bool = True,
upload_gdrive: Optional[bool] = None,
Expand Down Expand Up @@ -600,6 +605,7 @@ def generate_square_art(
fit_mode=fit_mode,
v_pos=v_pos,
zoom=zoom,
mirror=mirror,
)
return _persist_poster(
db,
Expand Down Expand Up @@ -643,6 +649,7 @@ def generate_background_art(
fit_mode: str = "cover",
v_pos: float = 0.0,
zoom: float = 1.0,
mirror: bool = False,
resolution: str = "1080p",
season_number: Optional[int] = None,
save_local: bool = True,
Expand Down Expand Up @@ -689,6 +696,7 @@ def generate_background_art(
fit_mode=fit_mode,
v_pos=v_pos,
zoom=zoom,
mirror=mirror,
)
return _persist_poster(
db,
Expand Down Expand Up @@ -1444,6 +1452,7 @@ def generate_seasons(
crop: Optional[Tuple[float, float, float, float]] = None,
v_pos: float = 0.0,
zoom: float = 1.0,
mirror: bool = False,
logo_scale: float = 1.0,
logo_y_offset: int = 0,
whiten: Optional[bool] = None, # None = module config (whiten_logo)
Expand Down Expand Up @@ -1499,6 +1508,7 @@ def generate_seasons(
crop=crop,
v_pos=v_pos,
zoom=zoom,
mirror=mirror,
logo_scale=logo_scale,
logo_y_offset=logo_y_offset,
whiten=whiten,
Expand Down Expand Up @@ -1552,6 +1562,7 @@ def psd_for_item(
crop: Optional[Tuple[float, float, float, float]] = None,
v_pos: float = 0.0,
zoom: float = 1.0,
mirror: bool = False,
whiten: Optional[bool] = None, # None = module config (whiten_logo)
flat_white: bool = False, # paint the logo a flat pure-white silhouette
logo_3d: bool = False, # extruded art -> flat-white lit face; wins over flat_white
Expand Down Expand Up @@ -1599,6 +1610,7 @@ def psd_for_item(
crop=crop,
v_pos=v_pos,
zoom=zoom,
mirror=mirror,
)
logo_bytes = custom_logo_bytes
if logo_bytes is None and logo_path:
Expand Down
26 changes: 25 additions & 1 deletion backend/util/cl2k/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,7 @@ def render_framed_art(
fit_mode: str = "cover",
v_pos: float = 0.0,
zoom: float = 1.0,
mirror: bool = False,
) -> bytes:
"""Render plain framed artwork at ``width``×``height`` — no gradient/logo/label.

Expand All @@ -957,7 +958,7 @@ def render_framed_art(
horizontally and ``v_pos`` (-1..1, 0 = centred) vertically, where the image
overflows the canvas; plain black letterbox where it doesn't. There is no
gradient here to hide an extended band, so ``v_pos`` is source-bounded both
ways. Encoded at CL2K quality.
ways. ``mirror`` flips the finished frame horizontally. Encoded at CL2K quality.
"""
zoom = max(geo.ZOOM_MIN, min(float(zoom or 1.0), geo.ZOOM_MAX))
with Image(blob=backdrop_bytes) as img:
Expand All @@ -977,6 +978,8 @@ def render_framed_art(
oy = -_v_pos_top(nh, height, v_pos) if nh >= height else (height - nh) // 2
with Image(width=width, height=height, background=Color("black")) as canvas:
canvas.composite(img, left=ox, top=oy)
if mirror:
canvas.flop()
return _encode_jpeg(canvas)


Expand All @@ -988,6 +991,7 @@ def render_square_art(
fit_mode: str = "cover",
v_pos: float = 0.0,
zoom: float = 1.0,
mirror: bool = False,
) -> bytes:
"""Render square (1:1) art from a backdrop/poster — just the framed artwork."""
return render_framed_art(
Expand All @@ -998,6 +1002,7 @@ def render_square_art(
fit_mode=fit_mode,
v_pos=v_pos,
zoom=zoom,
mirror=mirror,
)


Expand All @@ -1009,6 +1014,7 @@ def _framed_inset_base(
crop: Optional[Tuple[float, float, float, float]],
v_pos: float,
zoom: float,
mirror: bool,
) -> Image:
"""Frame the backdrop FULL-BLEED and return a full CANVAS image.

Expand All @@ -1025,6 +1031,11 @@ def _framed_inset_base(

render_cl2k and frame_backdrop both go through here, so they stay
pixel-identical (the PSD POSTER-layer parity the exporter relies on).

``mirror`` flips the framed artwork horizontally. It lands HERE, at the end
of framing, rather than on the source bytes: the AI text-removal mask and the
extend outpaint (modules.cl2k_maker) are brushed/built in source space, and
the logo/label the caller composites next must never come out backwards.
"""
base = Image(
width=geo.CANVAS_W, height=geo.CANVAS_H, background=Color(geo.BORDER_COLOR)
Expand All @@ -1035,6 +1046,8 @@ def _framed_inset_base(
else:
_cover_resize(art, geo.CANVAS_W, geo.CANVAS_H, focus_x, v_pos, zoom)
base.composite(art, left=0, top=0)
if mirror:
base.flop()
return base


Expand All @@ -1046,6 +1059,7 @@ def frame_backdrop(
crop: Optional[Tuple[float, float, float, float]] = None,
v_pos: float = 0.0,
zoom: float = 1.0,
mirror: bool = False,
) -> bytes:
"""Frame a backdrop to the 2:3 canvas exactly as :func:`render_cl2k` would
and return PNG bytes.
Expand All @@ -1062,6 +1076,7 @@ def frame_backdrop(
crop=crop,
v_pos=v_pos,
zoom=zoom,
mirror=mirror,
) as base:
base.format = "png"
return base.make_blob()
Expand Down Expand Up @@ -1089,6 +1104,7 @@ def render_cl2k(
crop: Optional[Tuple[float, float, float, float]] = None,
v_pos: float = 0.0,
zoom: float = 1.0,
mirror: bool = False,
band_label: str = "",
place_logo: bool = True,
text_logo_stroke: int = 0,
Expand All @@ -1114,6 +1130,9 @@ def render_cl2k(
optionally isolates the subject region first; the black bottom band is the
gradient/logo zone. ``v_pos`` applies here too, but on :func:`_fit_resize`'s
0..1 top-anchored scale (0 = top), not cover's -1..1.

``mirror`` flips the ARTWORK horizontally; the logo and label are composited
afterwards and stay the right way round.
"""
kind = kind.lower()
baseline = geo.logo_baseline(kind)
Expand All @@ -1127,6 +1146,7 @@ def render_cl2k(
crop=crop,
v_pos=v_pos,
zoom=zoom,
mirror=mirror,
) as base:
with Image(filename=str(geo.GRADIENT_PNG)) as grad:
base.composite(grad, left=0, top=0)
Expand Down Expand Up @@ -1341,6 +1361,9 @@ def main() -> None:
ap.add_argument(
"--no-whiten", action="store_true", help="keep the logo's original colours"
)
ap.add_argument(
"--mirror", action="store_true", help="flip the artwork horizontally"
)
ap.add_argument("--font", help="font file for text")
ap.add_argument("--out", required=True, help="output .jpg path")
args = ap.parse_args()
Expand All @@ -1360,6 +1383,7 @@ def main() -> None:
season_text=args.season_text,
logo_max_width=args.width,
whiten=not args.no_whiten,
mirror=args.mirror,
font_path=args.font,
)
with open(args.out, "wb") as fh:
Expand Down
Loading
Loading