ps1: official-style ICON0/PIC0/PIC1 artwork with PSN templates
Replaces the letterboxed 144x80 icon + cropped PIC1 with official-looking PS1 Classics art: - ICON0 (80x80): cover art composited into the bundled case frame (PS logo + 'PlayStation.' wordmark) from data/ps1_templates/ICON0.PNG. - PIC0 (310x180): a PSN-style info board drawn in code — title, IGDB description, ESRB badge (from IGDB age_ratings) and PS3/PSP badges. - PIC1 (480x272): the bundled launch background template. The converter now embeds PIC0, the PS1 tab previews all three images, and ps1_art pulls IGDB description/ESRB as best effort. Templates are git-ignored (user-supplied, like base.PBP) with a drawn-art fallback when missing.
This commit is contained in:
+257
-29
@@ -808,23 +808,63 @@ def compose_game_cover(key_art_bytes):
|
||||
return bytes(ba)
|
||||
|
||||
|
||||
def make_ps1_assets(cover_bytes):
|
||||
"""Build ICON0.PNG (144x80) and PIC1.PNG (480x272) from PS1 cover art.
|
||||
_PS1_TEMPLATE_DIR = config.DATA_DIR / "ps1_templates"
|
||||
|
||||
Returns ``(icon0_bytes, pic1_bytes)``, or ``(None, None)`` when the art
|
||||
can't be decoded (or Qt isn't available). ICON0 is the art letterboxed
|
||||
inside the icon frame; PIC1 is the art cropped to fill the launch
|
||||
background.
|
||||
|
||||
def _ps1_template_bytes(name):
|
||||
"""Read a bundled PS1 art template (``ICON0.PNG`` / ``PIC1.PNG``)."""
|
||||
try:
|
||||
p = _PS1_TEMPLATE_DIR / name
|
||||
if p.is_file():
|
||||
return p.read_bytes()
|
||||
except OSError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _slot_rect(img):
|
||||
"""Find the fully-transparent cover slot in an icon template image.
|
||||
|
||||
Returns ``(x, y, w, h)`` or ``None`` when the image has no transparent
|
||||
region (e.g. a flattened template)."""
|
||||
if img.isNull():
|
||||
return None
|
||||
w, h = img.width(), img.height()
|
||||
min_x = min_y = 1 << 30
|
||||
max_x = max_y = -1
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
if img.pixelColor(x, y).alpha() < 10:
|
||||
min_x, max_x = min(min_x, x), max(max_x, x)
|
||||
min_y, max_y = min(min_y, y), max(max_y, y)
|
||||
if max_x < min_x or max_y < min_y:
|
||||
return None
|
||||
return min_x, min_y, max_x - min_x + 1, max_y - min_y + 1
|
||||
|
||||
|
||||
def make_ps1_assets(cover_bytes, title="", description="", esrb=None):
|
||||
"""Build official-style PS1 Classics art from the cover: (icon0, pic0, pic1).
|
||||
|
||||
- ``ICON0`` (80x80): the cover composited into the bundled case template
|
||||
(PS button logo, ``PlayStation.`` wordmark, gradient frame).
|
||||
- ``PIC0`` (310x180): the PSN-style info board — title, description,
|
||||
ESRB rating badge and PS3/PSP platform badges.
|
||||
- ``PIC1`` (480x272): the bundled launch background (gradient + curve).
|
||||
|
||||
Returns ``(None, None, None)`` when the art can't be decoded or Qt is
|
||||
unavailable.
|
||||
"""
|
||||
try:
|
||||
from PySide6.QtCore import QBuffer, QByteArray, Qt
|
||||
from PySide6.QtGui import QImage, QPainter
|
||||
from PySide6.QtCore import QBuffer, QByteArray, QPointF, QRectF, Qt
|
||||
from PySide6.QtGui import (QColor, QFont, QFontMetrics, QImage,
|
||||
QLinearGradient, QPainter, QPainterPath,
|
||||
QPen, QPolygonF)
|
||||
except Exception: # noqa: BLE001 - headless API may lack PySide6
|
||||
return None, None
|
||||
return None, None, None
|
||||
|
||||
art = QImage.fromData(cover_bytes)
|
||||
if art.isNull():
|
||||
return None, None
|
||||
return None, None, None
|
||||
art = art.convertToFormat(QImage.Format_ARGB32)
|
||||
|
||||
def _save(img):
|
||||
@@ -835,43 +875,215 @@ def make_ps1_assets(cover_bytes):
|
||||
buf.close()
|
||||
return bytes(ba)
|
||||
|
||||
# ICON0: fit the cover inside 144x80 (transparent margins).
|
||||
icon = QImage(144, 80, QImage.Format_ARGB32)
|
||||
icon.fill(Qt.transparent)
|
||||
# --- ICON0: cover art inside the case template ---------------------------
|
||||
icon = QImage(80, 80, QImage.Format_ARGB32)
|
||||
icon.fill(QColor(0, 0, 0))
|
||||
p = QPainter(icon)
|
||||
scaled = art.scaled(144, 80, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
p.drawImage((144 - scaled.width()) // 2, (80 - scaled.height()) // 2, scaled)
|
||||
p.setRenderHint(QPainter.SmoothPixmapTransform, True)
|
||||
tmpl = QImage.fromData(_ps1_template_bytes("ICON0.PNG") or b"")
|
||||
if not tmpl.isNull():
|
||||
tmpl = tmpl.convertToFormat(QImage.Format_ARGB32)
|
||||
slot = _slot_rect(tmpl) or (0, 0, 80, 80)
|
||||
sx, sy, sw, sh = slot
|
||||
cover = art.scaled(sw, sh, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
|
||||
p.drawImage(sx + (sw - cover.width()) // 2, sy + (sh - cover.height()) // 2, cover)
|
||||
p.drawImage(0, 0, tmpl)
|
||||
else:
|
||||
cover = art.scaled(80, 80, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
p.drawImage((80 - cover.width()) // 2, (80 - cover.height()) // 2, cover)
|
||||
p.end()
|
||||
icon0 = _save(icon)
|
||||
|
||||
# PIC1: crop the cover to fill 480x272.
|
||||
# --- PIC0: PSN-style info board -----------------------------------------
|
||||
pic0 = _make_pic0(title, description, esrb, QImage, QPainter, QPainterPath,
|
||||
QColor, QFont, QFontMetrics, QLinearGradient, QPen,
|
||||
QPolygonF, QPointF, QRectF, Qt, _save)
|
||||
|
||||
# --- PIC1: bundled launch background ------------------------------------
|
||||
pic1 = _make_pic1(QImage, QPainter, QColor, QLinearGradient, Qt, _save)
|
||||
|
||||
return icon0, pic0, pic1
|
||||
|
||||
|
||||
def _make_pic1(QImage, QPainter, QColor, QLinearGradient, Qt, _save):
|
||||
pic = QImage(480, 272, QImage.Format_ARGB32)
|
||||
pic.fill(Qt.transparent)
|
||||
raw = _ps1_template_bytes("PIC1.PNG")
|
||||
tmpl = QImage.fromData(raw) if raw else QImage()
|
||||
if not tmpl.isNull():
|
||||
return _save(tmpl.convertToFormat(QImage.Format_ARGB32))
|
||||
# Fallback: a light lavender gradient with a subtle curve (same vibe as
|
||||
# the official background) for when the template image is missing.
|
||||
grad = QLinearGradient(0, 0, 0, 272)
|
||||
grad.setColorAt(0.0, QColor(235, 232, 238))
|
||||
grad.setColorAt(1.0, QColor(205, 200, 215))
|
||||
p = QPainter(pic)
|
||||
scaled = art.scaled(480, 272, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
|
||||
p.drawImage((480 - scaled.width()) // 2, (272 - scaled.height()) // 2, scaled)
|
||||
p.fillRect(0, 0, 480, 272, grad)
|
||||
p.setPen(QPen(QColor(170, 160, 185, 120), 3))
|
||||
path = QPainterPath()
|
||||
path.moveTo(-40, 300)
|
||||
path.quadTo(240, 60, 520, 300)
|
||||
p.drawPath(path)
|
||||
p.end()
|
||||
return _save(pic)
|
||||
|
||||
return _save(icon), _save(pic)
|
||||
|
||||
def _make_pic0(title, description, esrb, QImage, QPainter, QPainterPath, QColor,
|
||||
QFont, QFontMetrics, QLinearGradient, QPen, QPolygonF, QPointF,
|
||||
QRectF, Qt, _save):
|
||||
W, H = 310, 180
|
||||
img = QImage(W, H, QImage.Format_ARGB32)
|
||||
|
||||
# Dark gradient backdrop.
|
||||
grad = QLinearGradient(0, 0, 0, H)
|
||||
grad.setColorAt(0.0, QColor(38, 38, 40))
|
||||
grad.setColorAt(1.0, QColor(12, 12, 14))
|
||||
|
||||
p = QPainter(img)
|
||||
p.setRenderHint(QPainter.Antialiasing, True)
|
||||
p.setRenderHint(QPainter.TextAntialiasing, True)
|
||||
p.fillRect(0, 0, W, H, grad)
|
||||
|
||||
# Rounded border.
|
||||
p.setPen(QPen(QColor(110, 110, 115), 1))
|
||||
p.setBrush(Qt.NoBrush)
|
||||
p.drawRoundedRect(QRectF(4, 4, W - 8, H - 8), 9, 9)
|
||||
|
||||
# PlayStation button glyphs, top-right.
|
||||
_draw_ps_glyphs(p, W - 34, 12, 22, QColor(200, 200, 205), QRectF, QPointF,
|
||||
QPolygonF, QPen, Qt)
|
||||
|
||||
# Title (bold, upper-left).
|
||||
f_title = QFont("DejaVu Sans")
|
||||
f_title.setPixelSize(19)
|
||||
f_title.setBold(True)
|
||||
p.setFont(f_title)
|
||||
p.setPen(QColor(255, 255, 255))
|
||||
p.drawText(QRectF(18, 16, W - 64, 28), Qt.AlignLeft | Qt.AlignVCenter,
|
||||
title or "")
|
||||
|
||||
# Description (wrapped, below title).
|
||||
if description:
|
||||
f_desc = QFont("DejaVu Sans")
|
||||
f_desc.setPixelSize(12)
|
||||
p.setFont(f_desc)
|
||||
p.setPen(QColor(215, 215, 215))
|
||||
text = description.strip()
|
||||
fm = QFontMetrics(f_desc)
|
||||
# Rough 3-line budget (~40 chars/line at 12px across 272px).
|
||||
if fm.horizontalAdvance(text) > 3 * (W - 36):
|
||||
text = text[:130].rstrip() + "\u2026"
|
||||
p.drawText(QRectF(18, 46, W - 36, 74),
|
||||
Qt.AlignLeft | Qt.AlignTop | Qt.TextWordWrap, text)
|
||||
|
||||
# ESRB badge (bottom-left) + caption.
|
||||
if esrb:
|
||||
badge = esrb.strip()
|
||||
if badge:
|
||||
_draw_esrb(p, badge, 18, 140, QRectF, QColor, QFont, QFontMetrics, QPen, Qt)
|
||||
|
||||
# Platform badges (bottom-right).
|
||||
_draw_platform_badge(p, "PS3", W - 14 - 64, 146, QColor(94, 146, 210),
|
||||
QRectF, QColor, QFont, QFontMetrics, Qt)
|
||||
_draw_platform_badge(p, "PSP", W - 14 - 30, 146, QColor(212, 175, 55),
|
||||
QRectF, QColor, QFont, QFontMetrics, Qt)
|
||||
|
||||
p.end()
|
||||
return _save(img)
|
||||
|
||||
|
||||
def _draw_ps_glyphs(p, x, y, size, color, QRectF, QPointF, QPolygonF, QPen, Qt):
|
||||
"""Draw the four PlayStation face-button glyphs in a 2x2 grid."""
|
||||
cell = size / 2.0
|
||||
pen = QPen(color, max(1.0, size / 14.0))
|
||||
pen.setCapStyle(Qt.RoundCap)
|
||||
pen.setJoinStyle(Qt.RoundJoin)
|
||||
p.setPen(pen)
|
||||
p.setBrush(Qt.NoBrush)
|
||||
# Triangle (top-left).
|
||||
tri = QPolygonF([QPointF(x, y + cell), QPointF(x + cell, y), QPointF(x + cell, y + cell)])
|
||||
p.drawPolygon(tri)
|
||||
# Circle (top-right).
|
||||
p.drawEllipse(QRectF(x + cell, y, cell, cell))
|
||||
# Cross (bottom-left).
|
||||
p.drawLine(QPointF(x + 1, y + cell + 1), QPointF(x + cell - 1, y + size - 1))
|
||||
p.drawLine(QPointF(x + cell - 1, y + cell + 1), QPointF(x + 1, y + size - 1))
|
||||
# Square (bottom-right).
|
||||
p.drawRect(QRectF(x + cell, y + cell, cell, cell))
|
||||
|
||||
|
||||
def _draw_esrb(p, badge, x, y, QRectF, QColor, QFont, QFontMetrics, QPen, Qt):
|
||||
f = QFont("DejaVu Sans")
|
||||
f.setPixelSize(9)
|
||||
f.setBold(True)
|
||||
fm = QFontMetrics(f)
|
||||
text = badge if badge != "EVERYONE" else "EVERYONE"
|
||||
# Shrink until it fits the box (the official badge shows the word).
|
||||
size = 9
|
||||
while size > 5 and fm.horizontalAdvance(text) > 46:
|
||||
size -= 1
|
||||
f.setPixelSize(size)
|
||||
fm = QFontMetrics(f)
|
||||
rect = QRectF(x, y, 50, 24)
|
||||
p.setPen(QPen(QColor(20, 20, 20), 1))
|
||||
p.setBrush(QColor(255, 255, 255))
|
||||
p.drawRoundedRect(rect, 3, 3)
|
||||
p.setPen(QColor(20, 20, 20))
|
||||
p.setFont(f)
|
||||
p.drawText(rect, Qt.AlignCenter, text)
|
||||
# Caption.
|
||||
fc = QFont("DejaVu Sans")
|
||||
fc.setPixelSize(7)
|
||||
p.setFont(fc)
|
||||
p.setPen(QColor(150, 150, 155))
|
||||
p.drawText(QRectF(x + 56, y + 6, 120, 14),
|
||||
Qt.AlignLeft | Qt.AlignVCenter, "ESRB CONTENT RATING")
|
||||
|
||||
|
||||
def _draw_platform_badge(p, label, x, y, color, QRectF, QColor, QFont,
|
||||
QFontMetrics, Qt):
|
||||
f = QFont("DejaVu Sans")
|
||||
f.setPixelSize(10)
|
||||
f.setBold(True)
|
||||
rect = QRectF(x, y, 30, 16)
|
||||
p.setPen(Qt.NoPen)
|
||||
p.setBrush(QColor(26, 26, 28))
|
||||
p.drawRoundedRect(rect, 4, 4)
|
||||
p.setPen(color)
|
||||
p.setFont(f)
|
||||
p.drawText(rect, Qt.AlignCenter, label)
|
||||
|
||||
|
||||
def ps1_art(title, release_id, timeout=10):
|
||||
"""Fetch a PS1 game's cover and return ``(icon0, pic1)`` PNG bytes.
|
||||
"""Fetch a PS1 game's cover and build ``(icon0, pic0, pic1)`` PNG bytes.
|
||||
|
||||
Reuses the local cover cache when available, otherwise searches the PS1
|
||||
sources (libretro, ScreenScraper, IGDB, Wikipedia). Returns
|
||||
``(None, None)`` when no art can be produced.
|
||||
sources (libretro, ScreenScraper, IGDB, Wikipedia). The description and
|
||||
ESRB rating for the PIC0 info board come from IGDB (best effort).
|
||||
Returns ``(None, None, None)`` when no art can be produced.
|
||||
"""
|
||||
key = release_id or stable_key(title)
|
||||
try:
|
||||
path = fetch_cover(title, key, "ps1", timeout=timeout)
|
||||
except Exception: # noqa: BLE001
|
||||
return None, None
|
||||
return None, None, None
|
||||
if path is None:
|
||||
return None, None
|
||||
return None, None, None
|
||||
|
||||
description = ""
|
||||
esrb = None
|
||||
try:
|
||||
return make_ps1_assets(path.read_bytes())
|
||||
meta = get_game_metadata_igdb(title, timeout=timeout, system="ps1")
|
||||
if meta:
|
||||
description = meta.get("overview") or ""
|
||||
esrb = meta.get("esrb")
|
||||
except Exception: # noqa: BLE001 - metadata is best effort
|
||||
pass
|
||||
|
||||
try:
|
||||
return make_ps1_assets(path.read_bytes(), title=title,
|
||||
description=description, esrb=esrb)
|
||||
except OSError:
|
||||
return None, None
|
||||
return None, None, None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -1321,6 +1533,21 @@ def _igdb_date(ts):
|
||||
return ""
|
||||
|
||||
|
||||
# IGDB ``age_ratings.rating`` values for ESRB (category 1). 6=RP, 7=EC, 8=E,
|
||||
# 9=E10+, 10=T, 11=M, 12=AO. The badge uses the short letter form.
|
||||
_ESRB_BADGE = {6: "RP", 7: "EC", 8: "EVERYONE", 9: "EVERYONE 10+",
|
||||
10: "TEEN", 11: "MATURE", 12: "ADULTS ONLY"}
|
||||
|
||||
|
||||
def _igdb_esrb(g):
|
||||
for ar in g.get("age_ratings") or []:
|
||||
if not isinstance(ar, dict):
|
||||
continue
|
||||
if ar.get("category") == 1: # ESRB
|
||||
return _ESRB_BADGE.get(ar.get("rating"))
|
||||
return None
|
||||
|
||||
|
||||
def _igdb_game_meta(g):
|
||||
genres = [x.get("name") for x in (g.get("genres") or []) if isinstance(x, dict) and x.get("name")]
|
||||
devs, pubs = [], []
|
||||
@@ -1353,6 +1580,7 @@ def _igdb_game_meta(g):
|
||||
"genres": list(dict.fromkeys(genres)),
|
||||
"players": None,
|
||||
"rating": rating,
|
||||
"esrb": _igdb_esrb(g),
|
||||
"youtube": yt,
|
||||
}
|
||||
|
||||
@@ -1367,7 +1595,7 @@ def _igdb_search(query, limit=20, timeout=10, system="games"):
|
||||
f'search "{term}"; '
|
||||
"fields name, cover.image_id, first_release_date, summary, genres.name, "
|
||||
"involved_companies.company.name, involved_companies.developer, involved_companies.publisher, "
|
||||
"rating, videos.video_id; "
|
||||
"rating, videos.video_id, age_ratings.category, age_ratings.rating; "
|
||||
f"where platforms = ({platform_id}); "
|
||||
f"limit {max(limit, 1)};"
|
||||
)
|
||||
@@ -1426,7 +1654,7 @@ def game_metadata_by_id_igdb(game_id, timeout=10):
|
||||
q = (
|
||||
"fields name, cover.image_id, first_release_date, summary, genres.name, "
|
||||
"involved_companies.company.name, involved_companies.developer, involved_companies.publisher, "
|
||||
"rating, videos.video_id; "
|
||||
"rating, videos.video_id, age_ratings.category, age_ratings.rating; "
|
||||
f"where id = {int(game_id)}; limit 1;"
|
||||
)
|
||||
games = _igdb_post(q, timeout) or []
|
||||
|
||||
+40
-30
@@ -3366,9 +3366,9 @@ class Ps1ScanWorker(QObject):
|
||||
|
||||
|
||||
class Ps1PreviewWorker(QObject):
|
||||
"""Fetch a disc's cover art and build the EBOOT icon/background preview."""
|
||||
"""Fetch a disc's cover art and build the EBOOT art preview."""
|
||||
|
||||
result = Signal(int, str, object, object) # gen, game_id, icon0 bytes, pic1 bytes
|
||||
result = Signal(int, str, object, object, object) # gen, game_id, icon0, pic0, pic1
|
||||
finished = Signal()
|
||||
|
||||
def __init__(self, gen, title, game_id):
|
||||
@@ -3379,10 +3379,10 @@ class Ps1PreviewWorker(QObject):
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
icon0, pic1 = covers.ps1_art(self.title, self.game_id)
|
||||
icon0, pic0, pic1 = covers.ps1_art(self.title, self.game_id)
|
||||
except Exception: # noqa: BLE001
|
||||
icon0, pic1 = None, None
|
||||
self.result.emit(self.gen, self.game_id, icon0, pic1)
|
||||
icon0, pic0, pic1 = None, None, None
|
||||
self.result.emit(self.gen, self.game_id, icon0, pic0, pic1)
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
@@ -3427,8 +3427,12 @@ class Ps1Page(QWidget):
|
||||
self.preview_title.setStyleSheet("font-weight: bold;")
|
||||
self.preview_icon0 = QLabel()
|
||||
self.preview_icon0.setAlignment(Qt.AlignCenter)
|
||||
self.preview_icon0.setFixedSize(288, 160)
|
||||
self.preview_icon0.setFixedSize(160, 160)
|
||||
self.preview_icon0.setStyleSheet("background: #14151a; border: 1px solid #333; color: #888;")
|
||||
self.preview_pic0 = QLabel()
|
||||
self.preview_pic0.setAlignment(Qt.AlignCenter)
|
||||
self.preview_pic0.setFixedSize(310, 180)
|
||||
self.preview_pic0.setStyleSheet("background: #14151a; border: 1px solid #333; color: #888;")
|
||||
self.preview_pic1 = QLabel()
|
||||
self.preview_pic1.setAlignment(Qt.AlignCenter)
|
||||
self.preview_pic1.setFixedSize(480, 272)
|
||||
@@ -3439,16 +3443,24 @@ class Ps1Page(QWidget):
|
||||
pv.setContentsMargins(8, 0, 0, 0)
|
||||
pv.addWidget(QLabel("<b>Preview</b>"))
|
||||
pv.addWidget(self.preview_title)
|
||||
pv.addWidget(QLabel("ICON0 — XMB icon"))
|
||||
pv.addWidget(QLabel("ICON0 — XMB icon (80×80)"))
|
||||
pv.addWidget(self.preview_icon0)
|
||||
pv.addWidget(QLabel("PIC1 — launch background"))
|
||||
pv.addWidget(QLabel("PIC0 — info board (310×180)"))
|
||||
pv.addWidget(self.preview_pic0)
|
||||
pv.addWidget(QLabel("PIC1 — launch background (480×272)"))
|
||||
pv.addWidget(self.preview_pic1)
|
||||
pv.addStretch(1)
|
||||
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setWidget(preview)
|
||||
scroll.setFrameShape(QScrollArea.NoFrame)
|
||||
|
||||
splitter = QSplitter(Qt.Horizontal)
|
||||
splitter.addWidget(self.table)
|
||||
splitter.addWidget(preview)
|
||||
splitter.addWidget(scroll)
|
||||
splitter.setStretchFactor(0, 1)
|
||||
splitter.setSizes([640, 500])
|
||||
layout.addWidget(splitter, 1)
|
||||
|
||||
def _on_preview_changed(self):
|
||||
@@ -3466,6 +3478,8 @@ class Ps1Page(QWidget):
|
||||
self.preview_title.setText(e["name"])
|
||||
self.preview_icon0.setPixmap(QPixmap())
|
||||
self.preview_icon0.setText("Loading…")
|
||||
self.preview_pic0.setPixmap(QPixmap())
|
||||
self.preview_pic0.setText("Loading…")
|
||||
self.preview_pic1.setPixmap(QPixmap())
|
||||
self.preview_pic1.setText("Loading…")
|
||||
self._preview_gen += 1
|
||||
@@ -3484,34 +3498,30 @@ class Ps1Page(QWidget):
|
||||
self._preview_threads.append(thread)
|
||||
self._preview_workers.append(worker)
|
||||
|
||||
def _on_preview_result(self, gen, game_id, icon0, pic1):
|
||||
def _on_preview_result(self, gen, game_id, icon0, pic0, pic1):
|
||||
if gen != self._preview_gen:
|
||||
return
|
||||
self.preview_icon0.setText("")
|
||||
self.preview_pic0.setText("")
|
||||
self.preview_pic1.setText("")
|
||||
if icon0:
|
||||
pm = QPixmap()
|
||||
pm.loadFromData(icon0)
|
||||
self.preview_icon0.setPixmap(
|
||||
pm.scaled(self.preview_icon0.width(), self.preview_icon0.height(),
|
||||
Qt.KeepAspectRatio, Qt.SmoothTransformation))
|
||||
else:
|
||||
self.preview_icon0.setText("No art found")
|
||||
if pic1:
|
||||
pm = QPixmap()
|
||||
pm.loadFromData(pic1)
|
||||
self.preview_pic1.setPixmap(
|
||||
pm.scaled(self.preview_pic1.width(), self.preview_pic1.height(),
|
||||
Qt.KeepAspectRatio, Qt.SmoothTransformation))
|
||||
else:
|
||||
self.preview_pic1.setText("No art found — the default icon will be used")
|
||||
for label, data, fallback in (
|
||||
(self.preview_icon0, icon0, "No art found"),
|
||||
(self.preview_pic0, pic0, "No art found"),
|
||||
(self.preview_pic1, pic1, "No art found — the default icon will be used"),
|
||||
):
|
||||
if data:
|
||||
pm = QPixmap()
|
||||
pm.loadFromData(data)
|
||||
label.setPixmap(pm.scaled(label.width(), label.height(),
|
||||
Qt.KeepAspectRatio, Qt.SmoothTransformation))
|
||||
else:
|
||||
label.setText(fallback)
|
||||
|
||||
def _reset_preview(self, message):
|
||||
self.preview_title.setText(message)
|
||||
self.preview_icon0.setPixmap(QPixmap())
|
||||
self.preview_icon0.setText("")
|
||||
self.preview_pic1.setPixmap(QPixmap())
|
||||
self.preview_pic1.setText("")
|
||||
for label in (self.preview_icon0, self.preview_pic0, self.preview_pic1):
|
||||
label.setPixmap(QPixmap())
|
||||
label.setText("")
|
||||
|
||||
def set_dirs(self, ps1_dir, output_dir):
|
||||
self._ps1_dir = ps1_dir
|
||||
|
||||
+7
-5
@@ -872,6 +872,7 @@ def convert(
|
||||
cancel_event=None,
|
||||
complevel: int = 1,
|
||||
icon0: Optional[bytes] = None,
|
||||
pic0: Optional[bytes] = None,
|
||||
pic1: Optional[bytes] = None,
|
||||
) -> Path:
|
||||
"""Convert a PS1 disc image to ``out_path`` (an ``EBOOT.PBP``).
|
||||
@@ -880,9 +881,9 @@ def convert(
|
||||
``.iso``/``.img``/``.bin``. ``base_pbp`` must be a PSN PS1 classic EBOOT.
|
||||
``complevel`` is the DEFLATE level for the disc data (1-9; 0 = raw, which
|
||||
does not boot on real hardware and is only useful for diagnostics).
|
||||
``icon0`` (144x80 PNG) and ``pic1`` (480x272 PNG) override the generated
|
||||
icon/background; when absent the base PBP's ICON0 is reused and PIC1 is
|
||||
left empty.
|
||||
``icon0`` (80x80 PNG), ``pic0`` (310x180 PNG) and ``pic1`` (480x272 PNG)
|
||||
override the generated art; when absent the base PBP's ICON0 is reused
|
||||
and PIC0/PIC1 are left empty.
|
||||
"""
|
||||
if not base_pbp.is_file():
|
||||
raise ConversionError(f"base.pbp not found: {base_pbp}")
|
||||
@@ -916,7 +917,7 @@ def convert(
|
||||
"sfo": sfo,
|
||||
"icon0": icon0,
|
||||
"icon1": b"",
|
||||
"pic0": b"",
|
||||
"pic0": pic0 or b"",
|
||||
"pic1": pic1 or b"",
|
||||
"snd0": b"",
|
||||
"data_psp": data_psp,
|
||||
@@ -986,6 +987,7 @@ def convert_to_folder(
|
||||
cancel_event=None,
|
||||
complevel: int = 1,
|
||||
icon0: Optional[bytes] = None,
|
||||
pic0: Optional[bytes] = None,
|
||||
pic1: Optional[bytes] = None,
|
||||
) -> Path:
|
||||
"""Convert a PS1 disc into a game folder ready for /PSP/GAME.
|
||||
@@ -1000,7 +1002,7 @@ def convert_to_folder(
|
||||
|
||||
convert(source, title, game_id, base_pbp, folder / "EBOOT.PBP",
|
||||
progress_cb=progress_cb, cancel_event=cancel_event, complevel=complevel,
|
||||
icon0=icon0, pic1=pic1)
|
||||
icon0=icon0, pic0=pic0, pic1=pic1)
|
||||
return folder
|
||||
|
||||
|
||||
|
||||
+4
-4
@@ -240,17 +240,17 @@ class Ps1ConvertSource(Source):
|
||||
from . import covers, ps1_convert
|
||||
|
||||
# Pull the game's real cover art (libretro / ScreenScraper / IGDB /
|
||||
# Wikipedia) and bake it into ICON0 + PIC1 so the XMB shows the actual
|
||||
# game, not the default PS1 icon. Best effort: fall back to defaults.
|
||||
# Wikipedia) and bake it into ICON0 + PIC0 + PIC1 so the XMB shows the
|
||||
# official-style art, not the default PS1 icon. Best effort.
|
||||
if progress_cb is not None:
|
||||
progress_cb("Artwork", 0.0, "")
|
||||
icon0, pic1 = covers.ps1_art(title, game_id)
|
||||
icon0, pic0, pic1 = covers.ps1_art(title, game_id)
|
||||
|
||||
try:
|
||||
ps1_convert.convert_to_folder(
|
||||
source, title, game_id, self.base_pbp, dest_dir,
|
||||
progress_cb=cb, cancel_event=cancel_event,
|
||||
icon0=icon0, pic1=pic1,
|
||||
icon0=icon0, pic0=pic0, pic1=pic1,
|
||||
)
|
||||
except ps1_convert.Cancelled:
|
||||
raise TransferCancelled() from None
|
||||
|
||||
Reference in New Issue
Block a user