fix: DOCX camo strip vivid colors, cover page layout matching PDF, floating image fix

- 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
This commit is contained in:
nogueiraguh
2026-03-29 20:58:38 -03:00
parent 35a2bf786b
commit c81909d0a1

View File

@@ -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'''
<wp:anchor {nsdecls('wp','a','r','pic')}
distT="0" distB="0" distL="0" distR="0"
simplePos="0" relativeHeight="0" behindDoc="1"
locked="1" layoutInCell="1" allowOverlap="1">
<wp:simplePos x="0" y="0"/>
<wp:positionH relativeFrom="page"><wp:posOffset>0</wp:posOffset></wp:positionH>
<wp:positionV relativeFrom="page"><wp:posOffset>0</wp:posOffset></wp:positionV>
<wp:extent cx="{sw}" cy="{sh}"/>
<wp:wrapNone/>
<wp:docPr id="1" name="CamoStrip"/>
<a:graphic>
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:pic>
<pic:nvPicPr>
<pic:cNvPr id="0" name="strip.png"/>
<pic:cNvPicPr/>
</pic:nvPicPr>
<pic:blipFill>
<a:blip r:embed="{rId}"/>
<a:stretch><a:fillRect/></a:stretch>
</pic:blipFill>
<pic:spPr>
<a:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="{sw}" cy="{sh}"/>
</a:xfrm>
<a:prstGeom prst="rect"/>
</pic:spPr>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:anchor>'''
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()