Rewrite how trade offer png's are generated to try to reduce system resources. Only render the specific html code necessary and not the entire trade offer details page.

This commit is contained in:
badblocks 2025-03-28 00:42:41 -07:00
parent 15f8eb7cf4
commit d5f8345581
9 changed files with 239 additions and 85 deletions

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-03-22 04:08
# Generated by Django 5.1.2 on 2025-03-28 04:43
import django.db.models.deletion
from django.db import migrations, models

View file

@ -3,7 +3,7 @@ from django import template
register = template.Library()
@register.inclusion_tag('templatetags/trade_offer.html', takes_context=True)
def render_trade_offer(context, offer, screenshot_mode=False, show_friend_code=False, expanded=False):
def render_trade_offer(context, offer, expanded=False):
"""
Renders a trade offer including detailed trade acceptance information.
Freezes the through-model querysets to avoid extra DB hits.
@ -34,10 +34,8 @@ def render_trade_offer(context, offer, screenshot_mode=False, show_friend_code=F
'acceptances': acceptances,
'have_cards_available': have_cards_available,
'want_cards_available': want_cards_available,
'screenshot_mode': screenshot_mode,
'in_game_name': offer.initiated_by.in_game_name,
'friend_code': offer.initiated_by.friend_code,
'show_friend_code': show_friend_code,
'num_cards_available': len(have_cards_available) + len(want_cards_available),
}
@ -74,4 +72,52 @@ def action_button_class(state_value):
'REJECTED_BY_ACCEPTOR': 'btn btn-error',
}
# Return a default style if the state isn't in the mapping.
return mapping.get(state_value, 'btn btn-outline')
return mapping.get(state_value, 'btn btn-outline')
@register.inclusion_tag('templatetags/trade_offer_png.html', takes_context=True)
def render_trade_offer_png(context, offer, show_friend_code=False):
trade_offer_have_cards = list(offer.trade_offer_have_cards.all())
trade_offer_want_cards = list(offer.trade_offer_want_cards.all())
have_cards_available = [
card for card in trade_offer_have_cards
if card.quantity > card.qty_accepted
]
want_cards_available = [
card for card in trade_offer_want_cards
if card.quantity > card.qty_accepted
]
num_cards = max(len(have_cards_available), len(want_cards_available))
aspect_ratio = 1.91
base_height = (round(num_cards / 2) * 56) + 138
if (len(have_cards_available) + len(want_cards_available)) >= 4:
base_width = (4 * 144) + 96
else:
base_width = (2 * 144) + 128
if base_height > base_width:
image_height = base_height
image_width = int(round(image_height * aspect_ratio)) + 1
else:
image_width = base_width
image_height = int(round(image_width / aspect_ratio))
base_url = context.get('request').build_absolute_uri('/')
return {
'offer_pk': offer.pk,
'offer_hash': offer.hash,
'rarity_icon': offer.rarity_icon,
'initiated_by_email': offer.initiated_by.user.email,
'initiated_by_username': offer.initiated_by.user.username,
'have_cards_available': have_cards_available,
'want_cards_available': want_cards_available,
'in_game_name': offer.initiated_by.in_game_name,
'friend_code': offer.initiated_by.friend_code,
'show_friend_code': show_friend_code,
'num_cards_available': len(have_cards_available) + len(want_cards_available),
'image_width': image_width,
'image_height': image_height,
'base_url': base_url,
}

View file

@ -599,83 +599,47 @@ class TradeAcceptanceUpdateView(LoginRequiredMixin, FriendCodeRequiredMixin, Upd
class TradeOfferPNGView(View):
"""
Generate a PNG screenshot of the rendered trade offer detail page using Playwright.
This view loads the trade offer detail page, waits for the trade offer element to render,
simulates a click to expand extra badges, and then screenshots only the trade offer element.
This view loads the SVG representation generated by the trade_offer_svg template tag,
wraps it in a minimal HTML document, and converts it to PNG using Playwright.
"""
def get(self, request, *args, **kwargs):
# For demonstration purposes, get the first trade offer.
# In production, you might want to identify the offer via a URL parameter.
trade_offer = TradeOffer.objects.get(pk=kwargs['pk'])
if not trade_offer:
raise Http404("Trade offer not found")
from django.shortcuts import get_object_or_404
trade_offer = get_object_or_404(TradeOffer, pk=kwargs['pk'])
# Get the URL for the trade offer detail view.
detail_url = request.build_absolute_uri(
reverse_lazy("trade_offer_detail", kwargs={"pk": trade_offer.pk})+"?screenshot_mode=true"
from trades.templatetags import trade_offer_tags
# Generate context for the SVG template tag.
tag_context = trade_offer_tags.render_trade_offer_png(
{'request': request}, trade_offer, show_friend_code=True
)
# Use provided dimensions from the context
image_width = tag_context.get('image_width')
image_height = tag_context.get('image_height')
html = render_to_string("templatetags/trade_offer_png.html", tag_context)
# Launch Playwright to render the HTML and capture a screenshot.
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
viewport={"width": 1280, "height": 800},
browser = p.chromium.launch(
headless=True,
args=[
"--disable-gpu",
"--no-sandbox",
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--disable-gpu'
]
)
# If the request contains a Django session cookie,
# add it to the browser context to bypass the login screen.
session_cookie = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
if session_cookie:
cookie = {
"name": settings.SESSION_COOKIE_NAME,
"value": session_cookie,
"domain": request.get_host().split(':')[0],
"path": "/",
"httpOnly": True,
"secure": not settings.DEBUG,
}
context.add_cookies([cookie])
# Open a new page and navigate to the detail view.
page = context.new_page()
page.goto(detail_url, wait_until="networkidle")
# Inject CSS to force transparency.
page.add_style_tag(content="""
html, body, .bg-base-200 {
background-color: rgba(255, 255, 255, 0) !important;
}
""")
trade_offer_selector = ".trade-offer-card-screenshot"
page.wait_for_selector(trade_offer_selector)
# Simulate a click on the toggle button within the trade offer element
# to force the extra details (e.g., extra badges) to expand.
# We use a selector that targets the first svg with a "cursor-pointer" class inside the trade offer card.
toggle_selector = f"{trade_offer_selector} svg.cursor-pointer"
try:
toggle = page.query_selector(toggle_selector)
if toggle:
toggle.click()
# Wait for the CSS animation to complete (600ms as in your template)
page.wait_for_timeout(600)
except Exception:
# If the toggle is not found or clicking fails, proceed without expansion.
pass
# Locate the element containing the trade offer and capture its screenshot.
element = page.query_selector(trade_offer_selector)
if not element:
browser.close()
raise Http404("Trade offer element not found on page")
png_bytes = element.screenshot(type="png", omit_background=True)
context_browser = browser.new_context(viewport={"width": image_width, "height": image_height})
page = context_browser.new_page()
page.set_content(html, wait_until="networkidle")
element = page.wait_for_selector(".trade-offer-card-screenshot")
screenshot_bytes = element.screenshot(type="png", omit_background=True)
browser.close()
return HttpResponse(png_bytes, content_type="image/png")
return HttpResponse(screenshot_bytes, content_type="image/png")
class TradeOfferCreateConfirmView(LoginRequiredMixin, View):
"""