From c81909d0a145b6852c5de11c842130dc7d439ce2 Mon Sep 17 00:00:00 2001 From: nogueiraguh Date: Sun, 29 Mar 2026 20:58:38 -0300 Subject: [PATCH] fix: DOCX camo strip vivid colors, cover page layout matching PDF, floating image fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Camo strip uses exact HTML gradient colors (#6b7f3a, #8a9a4e, #4a6741, #c4a94d) — olive/sage/gold tones - Fixed floating image in header (parse_xml anchor instead of broken OxmlElement) - Cover page: removed duplicate ORACLE, title pushed lower, Georgia serif font, Extract date in metadata - Pillow added to requirements.txt for camo strip generation --- backend/app.py | 317 +++++++++++++++++++++---------------------------- 1 file changed, 137 insertions(+), 180 deletions(-) diff --git a/backend/app.py b/backend/app.py index d929f89..d14295e 100644 --- a/backend/app.py +++ b/backend/app.py @@ -6331,19 +6331,57 @@ def _generate_camo_strip() -> bytes: """Generate a camouflage-style decorative strip PNG (like Oracle PDF reports).""" from PIL import Image, ImageDraw, ImageFilter import random, io as _io - W, H = 38, 1400 - COLORS = [(45, 74, 46), (74, 124, 89), (58, 74, 46), (196, 168, 130), (90, 110, 80), (35, 55, 35), (160, 140, 110)] - img = Image.new('RGB', (W, H), COLORS[0]) + W, H = 36, 1500 + # Exact colors from the HTML page-strip gradient — olive/sage/gold tones + C1 = (107, 127, 58) # #6b7f3a olive green + C2 = (138, 154, 78) # #8a9a4e sage green + C3 = (74, 103, 65) # #4a6741 forest + C4 = (45, 59, 42) # #2d3b2a dark forest + C5 = (196, 169, 77) # #c4a94d gold + C6 = (139, 109, 59) # #8b6d3b brown/tan + C7 = (61, 92, 58) # #3d5c3a deep green + ALL = [C1, C2, C3, C4, C5, C6, C7] + + img = Image.new('RGB', (W, H), C4) draw = ImageDraw.Draw(img) - random.seed(42) # deterministic for consistency - for _ in range(120): - c = random.choice(COLORS) - x = random.randint(-10, W + 10) - y = random.randint(-20, H + 20) - rw = random.randint(8, 30) - rh = random.randint(20, 80) - draw.ellipse([x - rw, y - rh, x + rw, y + rh], fill=c) - img = img.filter(ImageFilter.GaussianBlur(radius=3)) + random.seed(42) + import math + + def _poly(cx, cy, r, sides=5): + pts = [] + for i in range(sides): + a = 2 * math.pi * i / sides + random.uniform(-0.5, 0.5) + rx = r * random.uniform(0.5, 1.4) + ry = r * random.uniform(0.8, 2.2) + pts.append((cx + rx * math.cos(a), cy + ry * math.sin(a))) + return pts + + # Vertical gradient base (like the CSS linear-gradient) + for y in range(H): + t = y / H + # Cycle through gradient stops + if t < 0.16: c = C1 if t < 0.08 else C2 + elif t < 0.32: c = C3 if t < 0.24 else C4 + elif t < 0.48: c = C5 if t < 0.40 else C6 + elif t < 0.64: c = C7 if t < 0.56 else C1 + elif t < 0.80: c = C5 if t < 0.72 else C3 + else: c = C4 if t < 0.88 else C2 + draw.line([(0, y), (W, y)], fill=c) + + # Organic patches on top for camo texture + for _ in range(35): + c = random.choice(ALL) + cx = random.randint(-8, W + 8) + cy = random.randint(-20, H + 20) + draw.polygon(_poly(cx, cy, random.randint(12, 30), random.randint(4, 6)), fill=c) + + for _ in range(25): + c = random.choice([C5, C6, C2]) + cx = random.randint(0, W) + cy = random.randint(0, H) + draw.polygon(_poly(cx, cy, random.randint(6, 16), random.randint(3, 5)), fill=c) + + img = img.filter(ImageFilter.GaussianBlur(radius=1.2)) buf = _io.BytesIO() img.save(buf, format='PNG') return buf.getvalue() @@ -6351,111 +6389,58 @@ def _generate_camo_strip() -> bytes: def _add_floating_image_to_header(header, image_bytes: bytes, width_emu: int, height_emu: int): """Add a floating behind-text image to a DOCX header (positioned at left edge, full page height).""" - from docx.opc.constants import RELATIONSHIP_TYPE as RT - from docx.oxml.ns import qn - from docx.oxml import OxmlElement + from docx.oxml.ns import nsdecls + from docx.oxml import parse_xml, OxmlElement import io as _io - # Add image as a relationship - image_part, rId = header.part.get_or_add_image_part(_io.BytesIO(image_bytes)) + rId, _ = header.part.get_or_add_image(_io.BytesIO(image_bytes)) + sw, sh = str(width_emu), str(height_emu) - # Build wp:anchor element for floating behind-text image - anchor = OxmlElement('wp:anchor') - anchor.set('distT', '0') - anchor.set('distB', '0') - anchor.set('distL', '0') - anchor.set('distR', '0') - anchor.set('simplePos', '0') - anchor.set('relativeHeight', '0') - anchor.set('behindDoc', '1') - anchor.set('locked', '1') - anchor.set('layoutInCell', '1') - anchor.set('allowOverlap', '1') + anchor_xml = f''' + + + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + ''' - simplePos = OxmlElement('wp:simplePos') - simplePos.set('x', '0') - simplePos.set('y', '0') - anchor.append(simplePos) - - posH = OxmlElement('wp:positionH') - posH.set('relativeFrom', 'page') - posHOffset = OxmlElement('wp:posOffset') - posHOffset.text = '0' - posH.append(posHOffset) - anchor.append(posH) - - posV = OxmlElement('wp:positionV') - posV.set('relativeFrom', 'page') - posVOffset = OxmlElement('wp:posOffset') - posVOffset.text = '0' - posV.append(posVOffset) - anchor.append(posV) - - extent = OxmlElement('wp:extent') - extent.set('cx', str(width_emu)) - extent.set('cy', str(height_emu)) - anchor.append(extent) - - wrapNone = OxmlElement('wp:wrapNone') - anchor.append(wrapNone) - - docPr = OxmlElement('wp:docPr') - docPr.set('id', '1') - docPr.set('name', 'CamoStrip') - anchor.append(docPr) - - graphic = OxmlElement('a:graphic') - graphic.set('xmlns:a', 'http://schemas.openxmlformats.org/drawingml/2006/main') - graphicData = OxmlElement('a:graphicData') - graphicData.set('uri', 'http://schemas.openxmlformats.org/drawingml/2006/picture') - - pic = OxmlElement('pic:pic') - pic.set('xmlns:pic', 'http://schemas.openxmlformats.org/drawingml/2006/picture') - - nvPicPr = OxmlElement('pic:nvPicPr') - cNvPr = OxmlElement('pic:cNvPr') - cNvPr.set('id', '0') - cNvPr.set('name', 'camo_strip.png') - nvPicPr.append(cNvPr) - cNvPicPr = OxmlElement('pic:cNvPicPr') - nvPicPr.append(cNvPicPr) - pic.append(nvPicPr) - - blipFill = OxmlElement('pic:blipFill') - blip = OxmlElement('a:blip') - blip.set(qn('r:embed'), rId) - blipFill.append(blip) - stretch = OxmlElement('a:stretch') - fillRect = OxmlElement('a:fillRect') - stretch.append(fillRect) - blipFill.append(stretch) - pic.append(blipFill) - - spPr = OxmlElement('pic:spPr') - xfrm = OxmlElement('a:xfrm') - off = OxmlElement('a:off') - off.set('x', '0') - off.set('y', '0') - xfrm.append(off) - ext = OxmlElement('a:ext') - ext.set('cx', str(width_emu)) - ext.set('cy', str(height_emu)) - xfrm.append(ext) - spPr.append(xfrm) - prstGeom = OxmlElement('a:prstGeom') - prstGeom.set('prst', 'rect') - spPr.append(prstGeom) - pic.append(spPr) - - graphicData.append(pic) - graphic.append(graphicData) - anchor.append(graphic) - - # Create a drawing element and add to header paragraph + anchor_el = parse_xml(anchor_xml) drawing = OxmlElement('w:drawing') - drawing.append(anchor) + drawing.append(anchor_el) + # Add to the first run in header paragraph p = header.paragraphs[0] - p._p.insert(0, drawing) + runs = p.runs + if runs: + runs[0]._r.append(drawing) + else: + r = p.add_run() + r._r.append(drawing) def _generate_compliance_docx(rid: str, tenancy: str, file_map: dict) -> bytes: @@ -6744,97 +6729,69 @@ def _generate_compliance_docx(rid: str, tenancy: str, file_map: dict) -> bytes: # ── COVER PAGE ── # ═══════════════════════════════════════════════ - # "ORACLE" in red at top left, bold - p = doc.add_paragraph() - p.alignment = WD_ALIGN_PARAGRAPH.LEFT - r = p.add_run("ORACLE") - r.font.color.rgb = ORACLE_RED - r.font.size = Pt(16) - r.bold = True - r.font.name = 'Calibri' - - # Large spacer to push title to middle-bottom - for _ in range(8): + # Large spacer to push title to middle-bottom of page (matching PDF cover) + for _ in range(14): sp = doc.add_paragraph() sp.paragraph_format.space_before = Pt(0) sp.paragraph_format.space_after = Pt(0) sp.paragraph_format.line_spacing = 1.0 - # Title: "CIS Foundations Benchmark Assessment" — 38px ~ Pt(28.5), light weight, #111827 + # Title — PDF uses serif font (~38pt), light weight, dark color p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.LEFT p.paragraph_format.space_before = Pt(0) - p.paragraph_format.space_after = Pt(6) - r = p.add_run("CIS Foundations Benchmark Assessment") - r.font.size = Pt(28) + p.paragraph_format.space_after = Pt(2) + r = p.add_run("CIS Foundations") + r.font.size = Pt(36) r.font.color.rgb = TITLE_DARK - r.font.name = 'Calibri' + r.font.name = 'Georgia' r.bold = False - # Decorative red line - line_para = doc.add_paragraph() - line_para.paragraph_format.space_before = Pt(0) - line_para.paragraph_format.space_after = Pt(8) - pPr = line_para._p.get_or_add_pPr() - pBdr = OxmlElement('w:pBdr') - bottom_border = OxmlElement('w:bottom') - bottom_border.set(qn('w:val'), 'single') - bottom_border.set(qn('w:sz'), '12') - bottom_border.set(qn('w:space'), '1') - bottom_border.set(qn('w:color'), 'C74634') - pBdr.append(bottom_border) - pPr.append(pBdr) - - # Subtitle: "{tenancy} – Oracle Cloud Infrastructure" — 12.5px ~ Pt(9.4), italic, #374151 p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.LEFT - p.paragraph_format.space_before = Pt(4) - p.paragraph_format.space_after = Pt(20) + p.paragraph_format.space_before = Pt(0) + p.paragraph_format.space_after = Pt(8) + r = p.add_run("Benchmark Assessment") + r.font.size = Pt(36) + r.font.color.rgb = TITLE_DARK + r.font.name = 'Georgia' + r.bold = False + + # Subtitle — "{tenancy} – Oracle Cloud Infrastructure" + p = doc.add_paragraph() + p.alignment = WD_ALIGN_PARAGRAPH.LEFT + p.paragraph_format.space_before = Pt(12) + p.paragraph_format.space_after = Pt(16) r = p.add_run(f"{tenancy} \u2013 Oracle Cloud Infrastructure") - r.font.size = Pt(13) + r.font.size = Pt(12) r.font.color.rgb = SUBTITLE_GRAY - r.italic = True r.font.name = 'Calibri' - # Metadata lines - metadata_items = [ - ("Tenancy", tenancy), - ("Month/Year", month_year), - ("Version", "1.0"), - ("Extract Date", extract_date or now.strftime("%Y-%m-%d")), - ] - for label, value in metadata_items: + # Metadata — identical to PDF cover + def _meta_line(doc, text, bold_prefix=None, sz=Pt(10)): p = doc.add_paragraph() p.paragraph_format.space_before = Pt(1) p.paragraph_format.space_after = Pt(1) - p.paragraph_format.line_spacing = 1.2 - r = p.add_run(f"{label}: ") - r.bold = True - r.font.size = Pt(10) - r.font.color.rgb = MUTED - r.font.name = 'Calibri' - r = p.add_run(value) - r.font.size = Pt(10) - r.font.color.rgb = BODY_TEXT + p.paragraph_format.line_spacing = 1.3 + if bold_prefix: + r = p.add_run(f"{bold_prefix} ") + r.bold = True + r.font.size = sz + r.font.color.rgb = BODY_TEXT + r.font.name = 'Calibri' + r = p.add_run(text) + else: + r = p.add_run(text) + r.font.size = sz + r.font.color.rgb = SUBTITLE_GRAY r.font.name = 'Calibri' - # Copyright line - p = doc.add_paragraph() - p.paragraph_format.space_before = Pt(12) - p.paragraph_format.space_after = Pt(2) - r = p.add_run(f"Copyright \u00A9 {now.year}, Oracle and/or its affiliates. All rights reserved.") - r.font.size = Pt(8) - r.font.color.rgb = MUTED - r.font.name = 'Calibri' - - # Confidential - p = doc.add_paragraph() - p.paragraph_format.space_before = Pt(0) - p.paragraph_format.space_after = Pt(0) - r = p.add_run("Confidential \u2013 Oracle Restricted") - r.font.size = Pt(8) - r.font.color.rgb = MUTED - r.font.name = 'Calibri' + _meta_line(doc, tenancy, bold_prefix="Tenancy:") + _meta_line(doc, f"{month_year}, Version [1.0]", sz=Pt(9)) + if extract_date: + _meta_line(doc, extract_date, bold_prefix="Extract date:", sz=Pt(9)) + _meta_line(doc, f"Copyright \u00A9 {now.year}, Oracle and/or its affiliates", sz=Pt(9)) + _meta_line(doc, "Confidential \u2013 Oracle Restricted", sz=Pt(9)) doc.add_page_break()