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:
317
backend/app.py
317
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'''
|
||||
<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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user