riad777 commited on
Commit
262eac3
·
verified ·
1 Parent(s): 3319866

Upload renderer.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. renderer.py +89 -32
renderer.py CHANGED
@@ -495,14 +495,16 @@ def render_layout_to_pdf(layout: Dict[str, Any],
495
  cover_bytes: Optional[bytes],
496
  page_count: int = 1,
497
  page_size_in: Tuple[float, float] = (PAGE_W_IN, PAGE_H_IN),
498
- output_path: Optional[Path] = None) -> bytes:
499
  """Render a layout JSON to a print-ready PDF.
500
 
501
  The cover is stamped on the FIRST page only (drawn as a raster
502
  image on top of the vector design). All subsequent pages are
503
  pure vector with the same layout.
504
 
505
- Returns raw PDF bytes (and writes to ``output_path`` if given).
 
 
506
  """
507
  page_w_in, page_h_in = page_size_in
508
 
@@ -515,6 +517,8 @@ def render_layout_to_pdf(layout: Dict[str, Any],
515
  buf = io.BytesIO()
516
  c = Canvas(buf, pagesize=(_in_to_pt(page_w_in), _in_to_pt(page_h_in)))
517
 
 
 
518
  for page_idx in range(page_count):
519
  # Start design below the safe zone
520
  y_pt = _in_to_pt(page_h_in - SAFE_TOP_IN)
@@ -523,13 +527,22 @@ def render_layout_to_pdf(layout: Dict[str, Any],
523
  c.setFillColor(HexColor(theme["bg"]))
524
  c.rect(0, 0, _in_to_pt(page_w_in), _in_to_pt(page_h_in), stroke=0, fill=1)
525
 
526
- # Cover band only on page 1
527
- if page_idx == 0 and cover_bytes:
528
- _stamp_cover_band(c, cover_bytes, page_w_in, page_h_in)
 
 
 
 
 
 
 
 
529
 
530
  # Subtle horizontal divider between cover band and design area
531
  # (acts as a hairline that ties the cover to the content).
532
- if page_idx == 0 and cover_bytes:
 
533
  c.setStrokeColor(HexColor(theme["primary"]))
534
  c.setLineWidth(0.5)
535
  divider_y_pt = _in_to_pt(page_h_in - SAFE_TOP_IN + 0.05)
@@ -574,51 +587,89 @@ def render_layout_to_pdf(layout: Dict[str, Any],
574
  pdf_bytes = buf.getvalue()
575
  if output_path is not None:
576
  output_path.write_bytes(pdf_bytes)
577
- return pdf_bytes
578
 
579
 
580
  def _stamp_cover_band(canvas: Canvas, cover_bytes: bytes,
581
- page_w_in: float, page_h_in: float) -> None:
582
  """Stamp the cover image into the top ``TOP_BANNER_INCHES`` inches.
583
 
584
- The cover is cropped to its visible *content* (not just the alpha
585
- bbox, which fails on white backgrounds) and then scaled to fill
586
- as much of the band as possible while preserving aspect ratio.
587
- The resized raster is drawn into the PDF; everything else on
588
- the page stays vector.
 
 
 
 
589
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
590
  if not cover_bytes:
591
- return
592
- cover_img = Image.open(io.BytesIO(cover_bytes))
 
 
 
 
593
  if cover_img.mode != "RGBA":
594
  cover_img = cover_img.convert("RGBA")
595
 
 
 
 
 
 
596
  # Content-aware crop: find the bounding box of pixels that are
597
  # actually drawn. Works for both transparent and white backgrounds.
598
  alpha = cover_img.split()[-1]
599
- src_w, src_h = cover_img.size
600
- # Threshold: any pixel with alpha > 32 is "solid content".
601
  solid = alpha.point(lambda a: 255 if a > 32 else 0)
602
  bbox = solid.getbbox()
603
- if bbox is not None:
604
- x0, y0, x1, y1 = bbox
605
- # Add a small breathing margin so the subject isn't flush
606
- # against the stamp edge.
607
- pad = max(2, int(min(x1 - x0, y1 - y0) * 0.01))
608
- x0 = max(0, x0 - pad)
609
- y0 = max(0, y0 - pad)
610
- x1 = min(src_w, x1 + pad)
611
- y1 = min(src_h, y1 + pad)
612
- cover_img = cover_img.crop((x0, y0, x1, y1))
613
- log.info("[cover] alpha bbox: %dx%d -> %dx%d (orig %dx%d)",
614
- x0, y0, x1, y1, src_w, src_h)
 
615
 
616
  src_w, src_h = cover_img.size
617
  if src_w == 0 or src_h == 0:
618
- log.warning("[cover] empty cover image; skipping stamp")
619
- return
620
 
621
- band_h_in = TOP_BANNER_INCHES
622
  src_aspect = src_w / src_h
623
 
624
  # Decide target size in INCHES. Use 95% of band height as the
@@ -633,6 +684,11 @@ def _stamp_cover_band(canvas: Canvas, cover_bytes: bytes,
633
  target_w_in = max_w_in
634
  target_h_in = target_w_in / src_aspect
635
 
 
 
 
 
 
636
  # Resize the raster to a high pixel density (450 DPI) so the
637
  # embedded image stays crisp in print.
638
  target_h_px = max(1, int(target_h_in * 450))
@@ -661,6 +717,7 @@ def _stamp_cover_band(canvas: Canvas, cover_bytes: bytes,
661
  "[cover] stamped %dx%d px (%.2fx%.2f in) at (%.2f, %.2f in)",
662
  target_w_px, target_h_px, target_w_in, target_h_in, x_in, y_in_bottom,
663
  )
 
664
 
665
 
666
  # ---------------------------------------------------------------------------
 
495
  cover_bytes: Optional[bytes],
496
  page_count: int = 1,
497
  page_size_in: Tuple[float, float] = (PAGE_W_IN, PAGE_H_IN),
498
+ output_path: Optional[Path] = None) -> Tuple[bytes, Dict[str, Any]]:
499
  """Render a layout JSON to a print-ready PDF.
500
 
501
  The cover is stamped on the FIRST page only (drawn as a raster
502
  image on top of the vector design). All subsequent pages are
503
  pure vector with the same layout.
504
 
505
+ Returns ``(pdf_bytes, info)`` where ``info`` is a dict with at
506
+ least ``{"cover_stamped": bool}`` so the caller can surface
507
+ cover-stamp failures to the user.
508
  """
509
  page_w_in, page_h_in = page_size_in
510
 
 
517
  buf = io.BytesIO()
518
  c = Canvas(buf, pagesize=(_in_to_pt(page_w_in), _in_to_pt(page_h_in)))
519
 
520
+ info: Dict[str, Any] = {"cover_stamped": False, "cover_warnings": []}
521
+
522
  for page_idx in range(page_count):
523
  # Start design below the safe zone
524
  y_pt = _in_to_pt(page_h_in - SAFE_TOP_IN)
 
527
  c.setFillColor(HexColor(theme["bg"]))
528
  c.rect(0, 0, _in_to_pt(page_w_in), _in_to_pt(page_h_in), stroke=0, fill=1)
529
 
530
+ # Cover band only on page 1. Always call the stamper on
531
+ # page 0 so a placeholder is drawn when the cover is
532
+ # missing/corrupt (rather than a silent blank top).
533
+ if page_idx == 0:
534
+ ok = _stamp_cover_band(c, cover_bytes or b"", page_w_in, page_h_in)
535
+ if page_idx == 0:
536
+ info["cover_stamped"] = bool(ok)
537
+ if not ok:
538
+ info["cover_warnings"].append(
539
+ "Cover stamp could not be drawn — a placeholder was rendered instead."
540
+ )
541
 
542
  # Subtle horizontal divider between cover band and design area
543
  # (acts as a hairline that ties the cover to the content).
544
+ # Only draw if a real cover (not a placeholder) was stamped.
545
+ if page_idx == 0 and info["cover_stamped"]:
546
  c.setStrokeColor(HexColor(theme["primary"]))
547
  c.setLineWidth(0.5)
548
  divider_y_pt = _in_to_pt(page_h_in - SAFE_TOP_IN + 0.05)
 
587
  pdf_bytes = buf.getvalue()
588
  if output_path is not None:
589
  output_path.write_bytes(pdf_bytes)
590
+ return pdf_bytes, info
591
 
592
 
593
  def _stamp_cover_band(canvas: Canvas, cover_bytes: bytes,
594
+ page_w_in: float, page_h_in: float) -> bool:
595
  """Stamp the cover image into the top ``TOP_BANNER_INCHES`` inches.
596
 
597
+ The cover is cropped to its visible *content* (alpha-based bbox)
598
+ and scaled to fill as much of the band as possible while
599
+ preserving aspect ratio. The resized raster is drawn into the
600
+ PDF; everything else on the page stays vector.
601
+
602
+ Returns ``True`` if a real cover image was stamped, ``False``
603
+ if a placeholder was drawn instead (or nothing was drawn).
604
+ Every early-return path logs a warning so the failure is visible
605
+ in the engine logs.
606
  """
607
+ band_h_in = TOP_BANNER_INCHES
608
+
609
+ def _draw_placeholder(reason: str) -> bool:
610
+ """Draw a thin dashed rectangle with a 'COVER' label so the
611
+ user can see the design intent instead of a blank top."""
612
+ x_in = SIDE_MARGIN_IN
613
+ w_in = page_w_in - 2 * SIDE_MARGIN_IN
614
+ h_in = band_h_in * 0.6
615
+ y_in = page_h_in - band_h_in + (band_h_in - h_in) / 2
616
+ x_pt = _in_to_pt(x_in)
617
+ y_pt = _in_to_pt(y_in)
618
+ w_pt = _in_to_pt(w_in)
619
+ h_pt = _in_to_pt(h_in)
620
+ canvas.setStrokeColor(HexColor("#9AA5A5"))
621
+ canvas.setLineWidth(0.6)
622
+ canvas.setDash(3, 3)
623
+ canvas.rect(x_pt, y_pt, w_pt, h_pt, stroke=1, fill=0)
624
+ canvas.setDash()
625
+ canvas.setFillColor(HexColor("#9AA5A5"))
626
+ canvas.setFont(DEFAULT_FONT_OBLIQUE, 10)
627
+ canvas.drawCentredString(
628
+ x_pt + w_pt / 2,
629
+ y_pt + h_pt / 2 - 3,
630
+ "[ cover unavailable — placeholder ]",
631
+ )
632
+ log.warning("[cover] placeholder drawn: %s", reason)
633
+ return False
634
+
635
  if not cover_bytes:
636
+ return _draw_placeholder("no cover bytes provided")
637
+
638
+ try:
639
+ cover_img = Image.open(io.BytesIO(cover_bytes))
640
+ except Exception as exc:
641
+ return _draw_placeholder(f"cannot open image: {exc}")
642
  if cover_img.mode != "RGBA":
643
  cover_img = cover_img.convert("RGBA")
644
 
645
+ src_w, src_h = cover_img.size
646
+ log.info("[cover] input: %dx%d px, mode=%s", src_w, src_h, cover_img.mode)
647
+ if src_w == 0 or src_h == 0:
648
+ return _draw_placeholder(f"zero-dimension image ({src_w}x{src_h})")
649
+
650
  # Content-aware crop: find the bounding box of pixels that are
651
  # actually drawn. Works for both transparent and white backgrounds.
652
  alpha = cover_img.split()[-1]
 
 
653
  solid = alpha.point(lambda a: 255 if a > 32 else 0)
654
  bbox = solid.getbbox()
655
+ if bbox is None:
656
+ return _draw_placeholder("no opaque content (alpha > 32) found in image")
657
+ x0, y0, x1, y1 = bbox
658
+ # Add a small breathing margin so the subject isn't flush
659
+ # against the stamp edge.
660
+ pad = max(2, int(min(x1 - x0, y1 - y0) * 0.01))
661
+ x0 = max(0, x0 - pad)
662
+ y0 = max(0, y0 - pad)
663
+ x1 = min(src_w, x1 + pad)
664
+ y1 = min(src_h, y1 + pad)
665
+ cover_img = cover_img.crop((x0, y0, x1, y1))
666
+ log.info("[cover] alpha bbox: %dx%d -> %dx%d (orig %dx%d)",
667
+ x0, y0, x1, y1, src_w, src_h)
668
 
669
  src_w, src_h = cover_img.size
670
  if src_w == 0 or src_h == 0:
671
+ return _draw_placeholder(f"zero-dim after crop ({src_w}x{src_h})")
 
672
 
 
673
  src_aspect = src_w / src_h
674
 
675
  # Decide target size in INCHES. Use 95% of band height as the
 
684
  target_w_in = max_w_in
685
  target_h_in = target_w_in / src_aspect
686
 
687
+ if target_h_in < 0.1 or target_w_in < 0.1:
688
+ return _draw_placeholder(
689
+ f"computed stamp too small ({target_w_in:.2f}x{target_h_in:.2f} in)"
690
+ )
691
+
692
  # Resize the raster to a high pixel density (450 DPI) so the
693
  # embedded image stays crisp in print.
694
  target_h_px = max(1, int(target_h_in * 450))
 
717
  "[cover] stamped %dx%d px (%.2fx%.2f in) at (%.2f, %.2f in)",
718
  target_w_px, target_h_px, target_w_in, target_h_in, x_in, y_in_bottom,
719
  )
720
+ return True
721
 
722
 
723
  # ---------------------------------------------------------------------------