The quality of root canal filling is most often estimated according to material ho mogeneity and the BS between the sealer and root canal dentin. Our study evaluated the porosity of a new CSBS, BioRoot Flow, after different laser-assisted protocols using a single-cone obturation technique in round canals. Regarding choosing the filling tech nique to ensure a tight junction between the dentinal walls and sealing material, there are no differences between the single-cone technique and the thermomechanical compaction technique in terms of the marginal gap when the root canal shape is rounded, but favorable results of thermomechanical compaction are noticed when the canal shape is oval, as stated by Zanza et al. [29]. Regarding material homogeneity in our study, porosity was observed in all tested samples, and micro-CT showed no statistical differences (p > 0.05) in the total void formation of BioRoot Flow CSBS after different disinfection protocols, namely two laser-assisted protocols (SWEEPS and diode laser), as well as a CI protocol; thus, the null hypothesis was accepted. In all groups, the total porosity was under 4%, which is https://doi.org/10.3390/bioengineering13060675 Bioengineering 2026, 13, 675 8 of 11 consistent with the results of other studies [30–32]. Inada et al. [32] compared the porosity of CSBS to that of AH plus and found that void formation in all groups was under 5%. A study by Nguyen et al. [30] observed less than 3% total voids when comparing CSBS with silicone bioactive glass-based and epoxy resin-based root canal sealers. The porosity was calculated after 30 and 60 days, which indicates the time dependence of CSBSs. In our study, the tests were done after 2 weeks, which possibly explains why the porosity showed slightly higher values. The porosity in all described studies was assessed by micro-CT. However, in our study, we compared different disinfection protocols in combination with the same CSBS (BioRoot Flow), and only in the SWEEPS group was the median percentage of voids less than 1% (0.3485%). Furthermore, the findings of Joshi et al. [33] showed that irradiation with an Er:YAG laser lowers the contact angle between CSBS and dentin, which could possibly have an effect on the BS and may also support further testing. Although laser-assisted protocols have been tested in various fields in recent years, to our knowledge there are no papers on the influence of laser on CSBS void formation. The benefits of further studies could be clinically important, especially for the treatment of root canals with morphological anomalies. Unlike void formation and porosity, the push-out BS has been analyzed more often in combination with different activated irrigation protocols. The results of our study showed that the SWEEPS laser-assisted protocol was beneficial with regard to the BS of the premixed CSBS (BioRoot Flow), which rejects the null hypothesis. The SWEEPS group outperformed CI (p < 0.05), but the diode laser group did not statistically differ from CI (p >0.05). Although there was no statistical difference between the SWEEPS and diode groups, SWEEPS showed higher median BS values (3.23 MPa) than the diode group (2.34 MPa), which aligns with the results of micro-CT analysis. Peker et al. [25] showed that sonic activation methods improve the BS of CSBS. Regarding laser-activated methods and BS, Bogari et al. [26] reported enhanced BS of CSBS after Nd:YAG laser treatment, and Bago et al. [34] showed that diode laser irradiation improves the dislocation resistance of a CSBS (BioRoot RCS). All results align with the findings of our study and support activation methods for enhancing the BS of bioceramic sealer. Despite the above mentioned results, the beneficial results of existing studies should be further examined due to the lack of information regarding the BS of CSBS after activation methods, especially laser-assisted ones. Although our study used SEM scanning for visualization and descriptive analysis, the results also correspond with other scientific data. Mahdi & Talabani [35] confirmed the bene ficial effect of activated irrigation methods, including diode laser irradiation, on bioceramic sealer penetration using SEM and confocal laser scanning microscopy (CLSM) analysis. One of the limitations of this study relates to the setting procedure of CSBS, which is specific due to its humidity requirements, and clinical conditions that cannot be recreated ex vivo, which may affect the results and, thus, their clinical applicability. Furthermore, considering the trend toward better results in the SWEEPS group and the findings of other studies, future examination should be conducted after a longer setting period [30]. For more than a decade, methodological problems of the push-out test have been pointed out. De-Deus [36] questions the use of 1 mm thin sliced specimens for testing root filling materials with two interfaces (core material and sealer), especially when ranking different materials, but nevertheless makes the point that push-out can contribute to understanding the properties of specific filling materials and their relationship to root dentin, which is exactly what this study aimed for. In addition, research showed that despite these limitations, the push-out test is still a suitable testing tool [37]. Moreover, the aim of this study was to evaluate the effect of different laser-assisted protocols rather than the material itself, and the protocols in all test groups were conducted under the same conditions. https://doi.org/10.3390/bioengineering13060675 Bioengineering 2026, 13, 675 9 of 11 The clinical significance of these results lies in proposing a protocol that can enhance the bonding ability of the premixed CSBSs and decrease their porosity, allowing the full potential of biomimetic root canal sealers to be reached.convert this image for my presentation
mkdir -p /tmp/workspace/root-canal-pptx && ls /tmp/workspace/root-canal-pptx/
~/root-canal-pptx/create_slides.py
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.util import Inches, Pt
from io import BytesIO
# Color palette - clinical/academic theme
DARK_BLUE = RGBColor(0x1A, 0x3A, 0x5C) # deep navy
MED_BLUE = RGBColor(0x2E, 0x6D, 0xA4) # medium blue
LIGHT_BLUE = RGBColor(0xD6, 0xE8, 0xF5) # pale blue background
ACCENT_TEAL = RGBColor(0x00, 0x8B, 0x8B) # teal accent
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
LIGHT_GRAY = RGBColor(0xF4, 0xF6, 0xF9)
MID_GRAY = RGBColor(0x78, 0x8B, 0x9A)
DARK_GRAY = RGBColor(0x33, 0x3F, 0x4A)
GREEN_ACC = RGBColor(0x27, 0xAE, 0x60)
ORANGE_ACC = RGBColor(0xE6, 0x7E, 0x22)
prs = Presentation()
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
blank_layout = prs.slide_layouts[6]
# ─────────────────────────────────────────────────────────
# HELPERS
# ─────────────────────────────────────────────────────────
def add_rect(slide, x, y, w, h, fill_rgb, alpha=None):
from pptx.util import Inches
shape = slide.shapes.add_shape(
1, # MSO_SHAPE_TYPE.RECTANGLE
Inches(x), Inches(y), Inches(w), Inches(h)
)
shape.line.fill.background()
shape.line.width = 0
fill = shape.fill
fill.solid()
fill.fore_color.rgb = fill_rgb
return shape
def add_textbox(slide, x, y, w, h, text, font_name="Calibri", font_size=14,
bold=False, italic=False, color=DARK_GRAY, align=PP_ALIGN.LEFT,
word_wrap=True, anchor=MSO_ANCHOR.TOP):
tb = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
tf = tb.text_frame
tf.word_wrap = word_wrap
tf.vertical_anchor = anchor
p = tf.paragraphs[0]
p.alignment = align
r = p.add_run()
r.text = text
r.font.name = font_name
r.font.size = Pt(font_size)
r.font.bold = bold
r.font.italic = italic
r.font.color.rgb = color
return tb
def add_multiline_textbox(slide, x, y, w, h, lines, font_name="Calibri",
font_size=12, bold=False, color=DARK_GRAY,
align=PP_ALIGN.LEFT, line_spacing_pt=None):
"""lines: list of (text, bold, font_size, color) tuples"""
tb = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
tf = tb.text_frame
tf.word_wrap = True
first = True
for (txt, b, fs, clr) in lines:
if first:
p = tf.paragraphs[0]
first = False
else:
p = tf.add_paragraph()
p.alignment = align
if line_spacing_pt:
from pptx.util import Pt as Pt2
from pptx.oxml.ns import qn
from lxml import etree
pPr = p._pPr
if pPr is None:
pPr = p._p.get_or_add_pPr()
lnSpc = etree.SubElement(pPr, qn('a:lnSpc'))
spcPts = etree.SubElement(lnSpc, qn('a:spcPts'))
spcPts.set('val', str(int(line_spacing_pt * 100)))
r = p.add_run()
r.text = txt
r.font.name = font_name
r.font.size = Pt(fs)
r.font.bold = b
r.font.color.rgb = clr
return tb
# ─────────────────────────────────────────────────────────
# SLIDE 1 – TITLE SLIDE
# ─────────────────────────────────────────────────────────
slide = prs.slides.add_slide(blank_layout)
# Dark navy header band
add_rect(slide, 0, 0, 13.333, 7.5, LIGHT_GRAY)
add_rect(slide, 0, 0, 13.333, 0.12, MED_BLUE) # top accent line
add_rect(slide, 0, 0, 0.08, 7.5, MED_BLUE) # left accent line
add_rect(slide, 0, 0, 13.333, 3.2, DARK_BLUE) # header band
# Title
add_textbox(slide, 0.5, 0.4, 12.3, 1.5,
"BioRoot Flow CSBS: Porosity & Bond Strength\nAfter Laser-Assisted Irrigation Protocols",
font_size=30, bold=True, color=WHITE, align=PP_ALIGN.CENTER,
anchor=MSO_ANCHOR.MIDDLE)
# Subtitle
add_textbox(slide, 0.5, 1.9, 12.3, 0.9,
"Single-Cone Obturation in Round Root Canals | Micro-CT & Push-Out Analysis",
font_size=15, bold=False, color=LIGHT_BLUE, align=PP_ALIGN.CENTER)
# Three key-finding boxes
box_y = 3.5
box_configs = [
("Porosity (Micro-CT)", "< 4% total voids in all groups\nSWEEPS: median 0.35% voids\nNo statistical differences (p > 0.05)", MED_BLUE),
("Bond Strength (Push-Out)", "SWEEPS > CI (p < 0.05)\nSWEEPS: 3.23 MPa median\nDiode: 2.34 MPa (p > 0.05 vs CI)", ACCENT_TEAL),
("Clinical Significance", "Laser activation enhances CSBS bonding\nSWEEPS is the preferred protocol\nSupports full use of biomimetic sealers", GREEN_ACC),
]
box_w = 3.9
box_gap = 0.2
start_x = 0.5
for i, (title, body, clr) in enumerate(box_configs):
bx = start_x + i*(box_w + box_gap)
# Card background
add_rect(slide, bx, box_y, box_w, 3.2, WHITE)
# Card top colour bar
add_rect(slide, bx, box_y, box_w, 0.08, clr)
# Card title
add_textbox(slide, bx+0.15, box_y+0.15, box_w-0.3, 0.55,
title, font_size=13, bold=True, color=clr)
# Card body
add_textbox(slide, bx+0.15, box_y+0.75, box_w-0.3, 2.3,
body, font_size=11, color=DARK_GRAY)
# Journal cite
add_textbox(slide, 0.5, 6.9, 12.3, 0.5,
"Bioengineering 2026, 13, 675 | https://doi.org/10.3390/bioengineering13060675",
font_size=9, italic=True, color=MID_GRAY, align=PP_ALIGN.CENTER)
# ─────────────────────────────────────────────────────────
# SLIDE 2 – STUDY BACKGROUND & METHODS
# ─────────────────────────────────────────────────────────
slide = prs.slides.add_slide(blank_layout)
add_rect(slide, 0, 0, 13.333, 7.5, LIGHT_GRAY)
add_rect(slide, 0, 0, 13.333, 0.08, MED_BLUE)
add_rect(slide, 0, 0, 0.08, 7.5, MED_BLUE)
# Slide header bar
add_rect(slide, 0, 0, 13.333, 1.1, DARK_BLUE)
add_textbox(slide, 0.3, 0.15, 12.5, 0.8,
"Study Background & Design",
font_size=24, bold=True, color=WHITE, anchor=MSO_ANCHOR.MIDDLE)
# Left column – Background
col1_x = 0.3
add_rect(slide, col1_x, 1.25, 6.0, 5.8, WHITE)
add_rect(slide, col1_x, 1.25, 6.0, 0.07, MED_BLUE)
add_textbox(slide, col1_x+0.2, 1.32, 5.6, 0.55,
"Background", font_size=14, bold=True, color=MED_BLUE)
bg_lines = [
("Root canal filling quality is judged by:", True, 11, DARK_BLUE),
(" • Material homogeneity", False, 11, DARK_GRAY),
(" • Bond strength (BS) between sealer & dentin", False, 11, DARK_GRAY),
("", False, 10, DARK_GRAY),
("Calcium silicate-based sealers (CSBS) set via moisture, produce hydroxyapatite, and bond chemically to dentin — enabling true biomimetic obturation.", False, 11, DARK_GRAY),
("", False, 10, DARK_GRAY),
("Single-cone vs thermomechanical compaction:", True, 11, DARK_BLUE),
(" • Round canals → no significant gap differences", False, 11, DARK_GRAY),
(" • Oval canals → thermomechanical compaction favoured (Zanza et al.)", False, 11, DARK_GRAY),
("", False, 10, DARK_GRAY),
("Irrigation activation may enhance sealer penetration and adhesion — but laser-specific data on CSBS void formation are lacking.", False, 11, DARK_GRAY),
]
add_multiline_textbox(slide, col1_x+0.2, 1.9, 5.6, 5.0, bg_lines)
# Right column – Study Design
col2_x = 6.8
add_rect(slide, col2_x, 1.25, 6.2, 5.8, WHITE)
add_rect(slide, col2_x, 1.25, 6.2, 0.07, ACCENT_TEAL)
add_textbox(slide, col2_x+0.2, 1.32, 5.8, 0.55,
"Study Design", font_size=14, bold=True, color=ACCENT_TEAL)
design_lines = [
("Sealer:", True, 11, DARK_BLUE),
(" BioRoot Flow — premixed CSBS", False, 11, DARK_GRAY),
("", False, 10, DARK_GRAY),
("Canal shape:", True, 11, DARK_BLUE),
(" Round canals, single-cone obturation", False, 11, DARK_GRAY),
("", False, 10, DARK_GRAY),
("Three Irrigation Groups:", True, 11, DARK_BLUE),
(" 1. SWEEPS laser (Er:YAG-based)", False, 11, DARK_GRAY),
(" 2. Diode laser", False, 11, DARK_GRAY),
(" 3. Conventional irrigation (CI) — control", False, 11, DARK_GRAY),
("", False, 10, DARK_GRAY),
("Outcome Measures:", True, 11, DARK_BLUE),
(" • Porosity / void formation → Micro-CT", False, 11, DARK_GRAY),
(" • Adhesion → Push-out bond strength test", False, 11, DARK_GRAY),
(" • Morphology → SEM imaging", False, 11, DARK_GRAY),
("", False, 10, DARK_GRAY),
("Setting period: 2 weeks (ex vivo)", True, 11, ORANGE_ACC),
]
add_multiline_textbox(slide, col2_x+0.2, 1.9, 5.8, 5.0, design_lines)
# ─────────────────────────────────────────────────────────
# SLIDE 3 – POROSITY / VOID FORMATION RESULTS
# ─────────────────────────────────────────────────────────
slide = prs.slides.add_slide(blank_layout)
add_rect(slide, 0, 0, 13.333, 7.5, LIGHT_GRAY)
add_rect(slide, 0, 0, 13.333, 0.08, MED_BLUE)
add_rect(slide, 0, 0, 0.08, 7.5, MED_BLUE)
add_rect(slide, 0, 0, 13.333, 1.1, DARK_BLUE)
add_textbox(slide, 0.3, 0.15, 12.5, 0.8,
"Results: Porosity & Void Formation (Micro-CT)",
font_size=24, bold=True, color=WHITE, anchor=MSO_ANCHOR.MIDDLE)
# Main finding banner
add_rect(slide, 0.3, 1.2, 12.7, 0.72, MED_BLUE)
add_textbox(slide, 0.3, 1.22, 12.7, 0.68,
"No statistically significant differences between groups (p > 0.05) — Null hypothesis ACCEPTED for porosity",
font_size=13, bold=True, color=WHITE, align=PP_ALIGN.CENTER,
anchor=MSO_ANCHOR.MIDDLE)
# Three group boxes
groups = [
("SWEEPS Protocol", "Median voids: 0.35%\n\nLowest porosity of all groups\n\nTrend toward superior performance", GREEN_ACC, "★ Best"),
("Diode Laser Protocol", "Total voids: < 4%\n\nNo significant difference from\nCI or SWEEPS (p > 0.05)\n\nIntermediate results", ORANGE_ACC, ""),
("Conventional Irrigation (CI)", "Total voids: < 4%\n\nControl reference group\n\nConsistent with literature\n(Nguyen et al.: < 3%; Inada et al.: < 5%)", MED_BLUE, ""),
]
gx = 0.3
gw = 4.1
gy = 2.1
gh = 3.6
gap = 0.2
for (gtitle, gbody, gclr, badge) in groups:
add_rect(slide, gx, gy, gw, gh, WHITE)
add_rect(slide, gx, gy, gw, 0.08, gclr)
add_textbox(slide, gx+0.15, gy+0.15, gw-0.3, 0.55,
gtitle, font_size=13, bold=True, color=gclr)
add_textbox(slide, gx+0.15, gy+0.75, gw-0.3, 2.7,
gbody, font_size=11, color=DARK_GRAY)
if badge:
add_rect(slide, gx+gw-1.1, gy+0.1, 0.95, 0.35, GREEN_ACC)
add_textbox(slide, gx+gw-1.08, gy+0.1, 0.92, 0.35,
badge, font_size=10, bold=True, color=WHITE,
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
gx += gw + gap
# Literature context box
add_rect(slide, 0.3, 5.85, 12.7, 1.3, WHITE)
add_rect(slide, 0.3, 5.85, 0.07, 1.3, ACCENT_TEAL)
lit_lines = [
("Literature Context:", True, 11, ACCENT_TEAL),
("Nguyen et al.: < 3% voids (CSBS vs silicone bioactive glass & epoxy resin sealers, assessed at 30 & 60 days — time dependence noted)", False, 10, DARK_GRAY),
("Inada et al.: < 5% voids (CSBS vs AH Plus) | Our study assessed at 2 weeks → slightly higher values expected vs longer-set studies", False, 10, DARK_GRAY),
("Joshi et al.: Er:YAG irradiation lowers CSBS-dentin contact angle → may further improve BS — warrants future investigation", False, 10, DARK_GRAY),
]
add_multiline_textbox(slide, 0.5, 5.9, 12.5, 1.2, lit_lines)
# ─────────────────────────────────────────────────────────
# SLIDE 4 – BOND STRENGTH RESULTS
# ─────────────────────────────────────────────────────────
slide = prs.slides.add_slide(blank_layout)
add_rect(slide, 0, 0, 13.333, 7.5, LIGHT_GRAY)
add_rect(slide, 0, 0, 13.333, 0.08, MED_BLUE)
add_rect(slide, 0, 0, 0.08, 7.5, MED_BLUE)
add_rect(slide, 0, 0, 13.333, 1.1, DARK_BLUE)
add_textbox(slide, 0.3, 0.15, 12.5, 0.8,
"Results: Push-Out Bond Strength",
font_size=24, bold=True, color=WHITE, anchor=MSO_ANCHOR.MIDDLE)
# Main finding banner
add_rect(slide, 0.3, 1.2, 12.7, 0.72, GREEN_ACC)
add_textbox(slide, 0.3, 1.22, 12.7, 0.68,
"SWEEPS outperforms CI (p < 0.05) — Null hypothesis REJECTED for bond strength",
font_size=13, bold=True, color=WHITE, align=PP_ALIGN.CENTER,
anchor=MSO_ANCHOR.MIDDLE)
# Bar chart simulation using rectangles
chart_x = 0.4
chart_y = 2.1
chart_h_max = 3.2 # height in inches for max value (3.23 MPa)
chart_w_area = 5.5
bar_w = 1.2
bar_gap = 0.8
bars = [
("SWEEPS", 3.23, GREEN_ACC),
("Diode Laser", 2.34, ORANGE_ACC),
("CI (Control)", 1.85, MED_BLUE), # approximate
]
max_val = 3.23
scale = chart_h_max / max_val
# Chart background
add_rect(slide, chart_x, chart_y, chart_w_area+0.3, chart_h_max+1.2, WHITE)
add_textbox(slide, chart_x+0.1, chart_y+0.1, chart_w_area, 0.4,
"Median Bond Strength (MPa) — Push-Out Test",
font_size=11, bold=True, color=DARK_BLUE)
bx_start = chart_x + 0.5
for i, (label, val, clr) in enumerate(bars):
bx = bx_start + i*(bar_w + bar_gap)
bar_h = val * scale
by = chart_y + 0.6 + (chart_h_max - bar_h)
add_rect(slide, bx, by, bar_w, bar_h, clr)
# Value label on top of bar
add_textbox(slide, bx, by-0.38, bar_w, 0.35,
f"{val} MPa", font_size=12, bold=True, color=clr,
align=PP_ALIGN.CENTER)
# x-axis label
add_textbox(slide, bx, chart_y+0.6+chart_h_max+0.05, bar_w, 0.4,
label, font_size=10, bold=False, color=DARK_GRAY,
align=PP_ALIGN.CENTER)
# Significance markers
# p < 0.05 between SWEEPS and CI
add_textbox(slide, bx_start+0.6, chart_y+0.52, 3.2, 0.35,
"* p < 0.05 (SWEEPS vs CI)",
font_size=10, bold=True, color=GREEN_ACC)
# Right panel – interpretation
rx = 6.2
add_rect(slide, rx, 2.0, 6.8, 5.1, WHITE)
add_rect(slide, rx, 2.0, 6.8, 0.07, GREEN_ACC)
add_textbox(slide, rx+0.2, 2.07, 6.4, 0.55,
"Key Findings & Context", font_size=14, bold=True, color=GREEN_ACC)
interp_lines = [
("Our Study Results:", True, 12, DARK_BLUE),
(" • SWEEPS: 3.23 MPa (highest — p < 0.05 vs CI)", False, 11, DARK_GRAY),
(" • Diode laser: 2.34 MPa (p > 0.05 vs CI)", False, 11, DARK_GRAY),
(" • CI: lowest BS — reference group", False, 11, DARK_GRAY),
("", False, 10, DARK_GRAY),
("Supporting Literature:", True, 12, DARK_BLUE),
(" • Peker et al.: sonic activation improves CSBS BS", False, 11, DARK_GRAY),
(" • Bogari et al.: Nd:YAG laser enhances CSBS BS", False, 11, DARK_GRAY),
(" • Bago et al.: diode laser improves dislocation resistance (BioRoot RCS)", False, 11, DARK_GRAY),
(" • Mahdi & Talabani: diode laser enhances sealer penetration (SEM/CLSM)", False, 11, DARK_GRAY),
("", False, 10, DARK_GRAY),
("Key Insight:", True, 12, ACCENT_TEAL),
("Activation methods — especially SWEEPS laser — consistently improve CSBS bond strength, aligning across multiple independent studies.", False, 11, DARK_GRAY),
("", False, 10, DARK_GRAY),
("Note on Push-Out Methodology:", True, 11, ORANGE_ACC),
("De-Deus raised concerns about 1 mm thin slices for dual-interface materials; however validated as suitable for protocol comparison (not material ranking).", False, 10, MID_GRAY),
]
add_multiline_textbox(slide, rx+0.2, 2.65, 6.5, 4.3, interp_lines)
# ─────────────────────────────────────────────────────────
# SLIDE 5 – LIMITATIONS & CONCLUSIONS
# ─────────────────────────────────────────────────────────
slide = prs.slides.add_slide(blank_layout)
add_rect(slide, 0, 0, 13.333, 7.5, LIGHT_GRAY)
add_rect(slide, 0, 0, 13.333, 0.08, MED_BLUE)
add_rect(slide, 0, 0, 0.08, 7.5, MED_BLUE)
add_rect(slide, 0, 0, 13.333, 1.1, DARK_BLUE)
add_textbox(slide, 0.3, 0.15, 12.5, 0.8,
"Limitations & Conclusions",
font_size=24, bold=True, color=WHITE, anchor=MSO_ANCHOR.MIDDLE)
# Left – Limitations
lx = 0.3
add_rect(slide, lx, 1.25, 5.9, 5.8, WHITE)
add_rect(slide, lx, 1.25, 5.9, 0.07, ORANGE_ACC)
add_textbox(slide, lx+0.2, 1.32, 5.5, 0.55,
"Limitations", font_size=14, bold=True, color=ORANGE_ACC)
lim_lines = [
("1. Ex vivo setting conditions", True, 11, DARK_BLUE),
(" CSBS requires specific humidity to set; clinical moisture cannot be fully reproduced ex vivo, limiting direct clinical translation.", False, 11, DARK_GRAY),
("", False, 10, DARK_GRAY),
("2. Short setting period (2 weeks)", True, 11, DARK_BLUE),
(" CSBS porosity is time-dependent. Nguyen et al. assessed at 30 & 60 days. Longer setting may yield lower void values — future studies needed.", False, 11, DARK_GRAY),
("", False, 10, DARK_GRAY),
("3. Push-out test methodology", True, 11, DARK_BLUE),
(" 1 mm slices with two interfaces (core + sealer) limit material ranking. Suitable here because all groups tested under identical conditions (protocol comparison).", False, 11, DARK_GRAY),
("", False, 10, DARK_GRAY),
("4. No data on CSBS + laser in oval canals", True, 11, DARK_BLUE),
(" Thermomechanical compaction favoured for oval canals. Laser CSBS data lacking for non-round morphologies.", False, 11, DARK_GRAY),
]
add_multiline_textbox(slide, lx+0.2, 1.92, 5.5, 5.0, lim_lines)
# Right – Conclusions
cx = 6.7
add_rect(slide, cx, 1.25, 6.3, 5.8, WHITE)
add_rect(slide, cx, 1.25, 6.3, 0.07, GREEN_ACC)
add_textbox(slide, cx+0.2, 1.32, 5.9, 0.55,
"Conclusions", font_size=14, bold=True, color=GREEN_ACC)
conc_lines = [
("Porosity (Null Hypothesis: Accepted)", True, 12, DARK_BLUE),
("No significant difference in void formation between SWEEPS, diode laser, and CI protocols (p > 0.05). All groups: total porosity < 4%.", False, 11, DARK_GRAY),
("SWEEPS showed the lowest median voids (0.35%) — numerically favourable.", False, 11, DARK_GRAY),
("", False, 10, DARK_GRAY),
("Bond Strength (Null Hypothesis: Rejected)", True, 12, DARK_BLUE),
("SWEEPS laser significantly outperformed CI (p < 0.05).", False, 11, DARK_GRAY),
("Diode laser did not differ significantly from CI but showed a positive trend.", False, 11, DARK_GRAY),
("", False, 10, DARK_GRAY),
("Clinical Recommendation:", True, 12, ACCENT_TEAL),
("SWEEPS laser-assisted irrigation is the recommended protocol when using BioRoot Flow CSBS — it enhances bonding and minimises porosity.", False, 11, DARK_GRAY),
("", False, 10, DARK_GRAY),
("Future Directions:", True, 12, MED_BLUE),
(" • Longer setting periods (30–60 days)", False, 11, DARK_GRAY),
(" • Oval canal morphology studies", False, 11, DARK_GRAY),
(" • Er:YAG laser contact angle effects on BS", False, 11, DARK_GRAY),
(" • In vivo / clinical validation", False, 11, DARK_GRAY),
]
add_multiline_textbox(slide, cx+0.2, 1.92, 6.0, 5.0, conc_lines)
# Bottom citation strip
add_rect(slide, 0, 7.15, 13.333, 0.35, DARK_BLUE)
add_textbox(slide, 0.3, 7.17, 12.7, 0.3,
"Bioengineering 2026, 13, 675 | doi: 10.3390/bioengineering13060675 | Laser-Assisted CSBS Protocol Study",
font_size=9, italic=True, color=LIGHT_BLUE, align=PP_ALIGN.CENTER)
# ─────────────────────────────────────────────────────────
# SAVE
# ─────────────────────────────────────────────────────────
out_path = "/tmp/workspace/root-canal-pptx/BioRoot_Flow_CSBS_Presentation.pptx"
buf = BytesIO()
prs.save(buf)
with open(out_path, "wb") as f:
f.write(buf.getvalue())
print(f"Saved: {out_path}")
python /tmp/workspace/root-canal-pptx/create_slides.py
BioRoot Flow CSBS Presentation
Presentation · PPTX
| Slide | Content |
|---|---|
| 1 - Title | Study title, 3 key-finding summary cards (porosity, bond strength, clinical significance) |
| 2 - Background & Design | CSBS rationale, single-cone vs compaction, 3 irrigation groups, outcome measures |
| 3 - Porosity Results (Micro-CT) | Per-group void data, null hypothesis outcome, literature comparison (Nguyen, Inada, Joshi) |
| 4 - Bond Strength Results | Visual bar chart (3.23 vs 2.34 MPa), p-value annotations, supporting literature panel |
| 5 - Limitations & Conclusions | 4 limitations, dual hypothesis summary, clinical recommendation, future directions |
all in one image
python3 -c "import subprocess; result = subprocess.run(['python3', '-c', 'import fitz; print(fitz.__version__)'], capture_output=True, text=True); print(result.stdout, result.stderr)"
python3 -c "from pptx import Presentation; from PIL import Image; import PIL; print('pptx ok, PIL ok', PIL.__version__)" python3 -c "import cairosvg; print('cairosvg ok')" 2>&1 || echo "no cairosvg" python3 -c "import reportlab; print('reportlab ok')" 2>&1 which convert 2>&1 || echo "no imagemagick" which libreoffice 2>&1 || echo "no libreoffice"
python3 -c " from pptx import Presentation from pptx.util import Inches, Pt prs = Presentation('/tmp/workspace/attachments/958287a7-9624-4643-a175-6fec30cb352d/BioRoot_Flow_CSBS_Presentation (1).pptx') print('Slides:', len(prs.slides)) print('Width:', prs.slide_width, 'Height:', prs.slide_height) for i, slide in enumerate(prs.slides): print(f'Slide {i+1}: {len(slide.shapes)} shapes') for s in slide.shapes: if s.has_text_frame: t = s.text_frame.text[:60].replace(chr(10),' ') print(f' text: {t}') "
~/root-canal-pptx/render_slides.py
"""
Render each slide of the PPTX to a PIL Image by reading shapes/fills/text
from python-pptx, then stack all slides into one tall PNG.
"""
from pptx import Presentation
from pptx.util import Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
from PIL import Image, ImageDraw, ImageFont
import os, textwrap
PPTX_PATH = '/tmp/workspace/attachments/958287a7-9624-4643-a175-6fec30cb352d/BioRoot_Flow_CSBS_Presentation (1).pptx'
OUT_PATH = '/tmp/workspace/root-canal-pptx/all_slides.png'
SCALE = 0.00009 # EMU → pixels (1 inch = 914400 EMU; 1 inch ≈ 82px here → nice ~960×540 per slide)
# Better scale: target ~1400px wide per slide
prs = Presentation(PPTX_PATH)
SW_emu = prs.slide_width # 12191695
SH_emu = prs.slide_height # 6858000
TARGET_W = 1400
SCALE = TARGET_W / SW_emu
SLIDE_W = int(SW_emu * SCALE)
SLIDE_H = int(SH_emu * SCALE)
print(f"Slide size: {SLIDE_W} x {SLIDE_H} px")
# ── font helpers ──────────────────────────────────────────────────────────────
FONT_CACHE = {}
def get_font(size_pt, bold=False, italic=False):
key = (size_pt, bold, italic)
if key in FONT_CACHE:
return FONT_CACHE[key]
# Try system fonts
candidates = []
if bold and italic:
candidates = ['/usr/share/fonts/truetype/dejavu/DejaVuSans-BoldOblique.ttf']
elif bold:
candidates = ['/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',
'/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf']
elif italic:
candidates = ['/usr/share/fonts/truetype/dejavu/DejaVuSans-Oblique.ttf']
else:
candidates = ['/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
'/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf']
# pixel size ≈ pt * 96/72
px = max(8, int(size_pt * 96 / 72 * SCALE / (914400 * SCALE / TARGET_W) * 1.0))
# simpler: pt → px at 96dpi then scale
px = max(8, int(size_pt * 1.33 * SCALE * 914400 / 72))
for path in candidates:
if os.path.exists(path):
try:
f = ImageFont.truetype(path, px)
FONT_CACHE[key] = f
return f
except Exception:
pass
f = ImageFont.load_default()
FONT_CACHE[key] = f
return f
def emu_to_px(emu):
return int(emu * SCALE)
def rgb_from_pptx(color_obj):
"""Return (r,g,b) tuple or None."""
try:
if color_obj and color_obj.type is not None:
rgb = color_obj.rgb
return (rgb.red, rgb.green, rgb.blue)
except Exception:
pass
return None
def draw_shape(draw, shape):
"""Draw a single shape onto the PIL ImageDraw context."""
# ── position ──
l = emu_to_px(shape.left) if shape.left else 0
t = emu_to_px(shape.top) if shape.top else 0
w = emu_to_px(shape.width) if shape.width else 0
h = emu_to_px(shape.height) if shape.height else 0
# ── background fill ──
fill_color = None
try:
fill = shape.fill
if fill.type is not None:
c = rgb_from_pptx(fill.fore_color)
if c:
fill_color = c
except Exception:
pass
if fill_color and w > 0 and h > 0:
draw.rectangle([l, t, l+w, t+h], fill=fill_color)
# ── text ──
if not shape.has_text_frame:
return
tf = shape.text_frame
# vertical anchor
anchor = 'top'
try:
from pptx.enum.text import MSO_ANCHOR
if tf.vertical_anchor == MSO_ANCHOR.MIDDLE:
anchor = 'middle'
elif tf.vertical_anchor == MSO_ANCHOR.BOTTOM:
anchor = 'bottom'
except Exception:
pass
# collect paragraphs → rendered lines
rendered = [] # list of (text, font, color_tuple, align)
for para in tf.paragraphs:
parts = []
p_bold = False
p_size = 12
p_color = (51, 63, 74) # DARK_GRAY default
p_align = PP_ALIGN.LEFT
try:
p_align = para.alignment or PP_ALIGN.LEFT
except Exception:
pass
for run in para.runs:
txt = run.text
if not txt:
continue
rfont = run.font
b = rfont.bold if rfont.bold is not None else False
it = rfont.italic if rfont.italic is not None else False
sz = rfont.size
sz_pt = (sz / 12700) if sz else 12 # EMU to pt
c = rgb_from_pptx(rfont.color) or p_color
parts.append((txt, sz_pt, b, it, c))
if b: p_bold = True
p_size = sz_pt
p_color = c
if not parts:
# empty paragraph → blank line
rendered.append(('', get_font(10), p_color, p_align))
continue
# For simplicity, treat each para as one line (join run texts)
full_text = ''.join(r[0] for r in parts)
# Use the first run's style for font
_, sz_pt, b, it, c = parts[0]
rendered.append((full_text, get_font(sz_pt, b, it), c, p_align))
if not rendered:
return
# measure total height for vertical anchor
line_h_list = []
for (txt, font, c, align) in rendered:
try:
bb = font.getbbox('Ag')
lh = bb[3] - bb[1] + 2
except Exception:
lh = 14
line_h_list.append(lh)
total_h = sum(line_h_list)
if anchor == 'middle':
y_start = t + max(0, (h - total_h) // 2)
elif anchor == 'bottom':
y_start = t + max(0, h - total_h)
else:
y_start = t + 2 # small top padding
# clip to shape bounds
y_cursor = y_start
for i, (txt, font, color, align) in enumerate(rendered):
lh = line_h_list[i]
if y_cursor + lh > t + h + 10:
break # overflow
# word-wrap within shape width
if not txt.strip():
y_cursor += lh
continue
# Estimate chars per line
try:
sample_w = font.getlength('m') if hasattr(font, 'getlength') else 8
except Exception:
sample_w = 8
chars_per_line = max(10, int(w / max(1, sample_w))) if sample_w > 0 else 40
wrapped = textwrap.wrap(txt, width=chars_per_line) or [txt]
for line in wrapped:
if y_cursor + lh > t + h + 10:
break
# x position based on alignment
try:
line_w = font.getlength(line) if hasattr(font, 'getlength') else len(line)*7
except Exception:
line_w = len(line) * 7
if align == PP_ALIGN.CENTER:
x = l + max(0, (w - line_w) // 2)
elif align == PP_ALIGN.RIGHT:
x = l + max(0, w - line_w - 4)
else:
x = l + 4 # LEFT + small indent
draw.text((x, y_cursor), line, font=font, fill=color)
y_cursor += lh
# If no wrapping happened (single line), advance
if len(wrapped) == 0:
y_cursor += lh
# ── render each slide ─────────────────────────────────────────────────────────
slide_images = []
for idx, slide in enumerate(prs.slides):
img = Image.new('RGB', (SLIDE_W, SLIDE_H), (244, 246, 249))
draw = ImageDraw.Draw(img)
for shape in slide.shapes:
try:
draw_shape(draw, shape)
except Exception as e:
pass # skip broken shapes silently
# thin slide-number label
fn = get_font(9)
draw.text((SLIDE_W - 60, SLIDE_H - 16), f"Slide {idx+1}/5", font=fn, fill=(150,150,150))
slide_images.append(img)
print(f" Rendered slide {idx+1}")
# ── stack vertically with a thin separator ───────────────────────────────────
SEP = 6 # px between slides
total_h = SLIDE_H * len(slide_images) + SEP * (len(slide_images) - 1)
combined = Image.new('RGB', (SLIDE_W, total_h), (30, 30, 40))
y = 0
for img in slide_images:
combined.paste(img, (0, y))
y += SLIDE_H + SEP
combined.save(OUT_PATH, 'PNG', optimize=True)
print(f"\nSaved combined image: {OUT_PATH}")
print(f"Final size: {combined.size}")
python3 /tmp/workspace/root-canal-pptx/render_slides.py 2>&1
ls -lh /tmp/workspace/root-canal-pptx/all_slides.png && python3 -c " from PIL import Image img = Image.open('/tmp/workspace/root-canal-pptx/all_slides.png') # check a sample pixel at top of slide 1 (should be dark blue header) px = img.getpixel((700, 50)) print('Sample pixel at header area:', px) px2 = img.getpixel((700, 400)) print('Sample pixel at body area:', px2) print('Image mode:', img.mode, 'Size:', img.size) "
python3 -c " from pptx import Presentation from pptx.util import Pt from pptx.oxml.ns import qn from lxml import etree prs = Presentation('/tmp/workspace/attachments/958287a7-9624-4643-a175-6fec30cb352d/BioRoot_Flow_CSBS_Presentation (1).pptx') slide = prs.slides[0] for i, shape in enumerate(slide.shapes[:8]): print(f'=== Shape {i}: {shape.shape_type} name={shape.name} left={shape.left} top={shape.top} w={shape.width} h={shape.height}') try: fill = shape.fill print(f' fill.type={fill.type}') try: print(f' fill.fore_color.rgb={fill.fore_color.rgb}') except Exception as e: print(f' fore_color error: {e}') except Exception as e: print(f' fill error: {e}') # print raw XML snippet xml = etree.tostring(shape.element, pretty_print=True).decode()[:400] print(xml[:400]) print() "
python3 -c " from pptx import Presentation from pptx.oxml.ns import qn from lxml import etree prs = Presentation('/tmp/workspace/attachments/958287a7-9624-4643-a175-6fec30cb352d/BioRoot_Flow_CSBS_Presentation (1).pptx') slide = prs.slides[0] shapes = list(slide.shapes) print('Num shapes:', len(shapes)) for i, shape in enumerate(shapes[:6]): print(f'=== Shape {i}: type={shape.shape_type} name={shape.name}') try: fill = shape.fill print(f' fill type: {fill.type}') try: print(f' fore_color rgb: {fill.fore_color.rgb}') except Exception as e2: print(f' fore_color err: {e2}') except Exception as e: print(f' fill err: {e}') xml_snippet = etree.tostring(shape._element, pretty_print=True).decode()[:500] print(xml_snippet) "
~/root-canal-pptx/render_slides2.py
"""
Render each PPTX slide to PIL Image, then stack into one tall PNG.
"""
from pptx import Presentation
from pptx.util import Pt
from pptx.enum.text import PP_ALIGN
from pptx.enum.dml import MSO_THEME_COLOR
from PIL import Image, ImageDraw, ImageFont
import textwrap, os
PPTX_PATH = '/tmp/workspace/attachments/958287a7-9624-4643-a175-6fec30cb352d/BioRoot_Flow_CSBS_Presentation (1).pptx'
OUT_PATH = '/tmp/workspace/root-canal-pptx/all_slides.png'
prs = Presentation(PPTX_PATH)
SW_emu = prs.slide_width
SH_emu = prs.slide_height
TARGET_W = 1400
SCALE = TARGET_W / SW_emu
SLIDE_W = int(SW_emu * SCALE)
SLIDE_H = int(SH_emu * SCALE)
print(f"Slide px: {SLIDE_W} x {SLIDE_H}")
# ── fonts ─────────────────────────────────────────────────────────────────────
FONT_DIR = '/usr/share/fonts/truetype/dejavu/'
_FONTS = {}
def get_font(size_pt, bold=False, italic=False):
key = (round(size_pt, 1), bold, italic)
if key in _FONTS:
return _FONTS[key]
# pt → px at 96 dpi, then scale to our canvas
px = max(9, int(size_pt * 96 / 72 * SCALE * (SW_emu / TARGET_W) * 0.93))
# simpler direct: pt * constant
# 1 pt = 1/72 inch; at 96dpi = 96/72 px; scaled = that * SCALE * SW_emu
# actually: emu_to_px(size_pt * 12700) → px
px = max(9, int(size_pt * 12700 * SCALE))
candidates = []
if bold and italic:
candidates = [FONT_DIR+'DejaVuSans-BoldOblique.ttf']
elif bold:
candidates = [FONT_DIR+'DejaVuSans-Bold.ttf']
elif italic:
candidates = [FONT_DIR+'DejaVuSans-Oblique.ttf']
else:
candidates = [FONT_DIR+'DejaVuSans.ttf']
candidates.append(FONT_DIR+'DejaVuSans.ttf')
for p in candidates:
if os.path.exists(p):
try:
f = ImageFont.truetype(p, px)
_FONTS[key] = f
return f
except Exception:
pass
f = ImageFont.load_default()
_FONTS[key] = f
return f
def ep(emu):
"""EMU → pixel"""
return int(emu * SCALE)
def get_rgb(fill):
"""Return (r,g,b) or None from a pptx FillFormat."""
try:
from pptx.enum.dml import MSO_FILL
if fill.type == MSO_FILL.SOLID:
rgb = fill.fore_color.rgb
return (rgb.red, rgb.green, rgb.blue)
except Exception:
pass
return None
def get_run_color(run):
try:
rgb = run.font.color.rgb
return (rgb.red, rgb.green, rgb.blue)
except Exception:
return None
def draw_slide(slide):
img = Image.new('RGB', (SLIDE_W, SLIDE_H), (244, 246, 249))
draw = ImageDraw.Draw(img)
for shape in slide.shapes:
try:
l = ep(shape.left or 0)
t = ep(shape.top or 0)
w = ep(shape.width or 0)
h = ep(shape.height or 0)
r_px = l + w
b_px = t + h
# ── fill rectangle ──────────────────────────────────────────────
fill_c = get_rgb(shape.fill) if hasattr(shape, 'fill') else None
if fill_c and w > 0 and h > 0:
draw.rectangle([l, t, r_px, b_px], fill=fill_c)
# ── text ─────────────────────────────────────────────────────────
if not shape.has_text_frame:
continue
tf = shape.text_frame
# vertical anchor
try:
from pptx.enum.text import MSO_ANCHOR
va = tf.vertical_anchor
v_middle = (va == MSO_ANCHOR.MIDDLE)
v_bottom = (va == MSO_ANCHOR.BOTTOM)
except Exception:
v_middle = v_bottom = False
# build list of (text_line, font, color_rgb, align)
para_items = []
for para in tf.paragraphs:
try:
align = para.alignment or PP_ALIGN.LEFT
except Exception:
align = PP_ALIGN.LEFT
# gather run texts & pick dominant style
texts = []
sz_pt = 12.0
bold = False
italic= False
color = (51, 63, 74)
for run in para.runs:
rt = run.text
if not rt:
continue
texts.append(rt)
rf = run.font
if rf.size:
sz_pt = rf.size / 12700.0
if rf.bold is not None:
bold = rf.bold
if rf.italic is not None:
italic = rf.italic
rc = get_run_color(run)
if rc:
color = rc
txt = ''.join(texts)
font = get_font(sz_pt, bold, italic)
para_items.append((txt, font, color, align, sz_pt))
if not para_items:
continue
# measure line heights
def lh_of(font, sz_pt):
try:
bb = font.getbbox('Ag')
return bb[3] - bb[1] + max(2, int(sz_pt * SCALE * 12700 * 0.25))
except Exception:
return max(12, int(sz_pt * 12700 * SCALE * 1.2))
heights = [lh_of(fnt, sz) for _, fnt, _, _, sz in para_items]
total_txt_h = sum(heights)
if v_middle:
y_cur = t + max(0, (h - total_txt_h) // 2)
elif v_bottom:
y_cur = t + max(0, h - total_txt_h - 4)
else:
y_cur = t + 3
for idx, (txt, font, color, align, sz_pt) in enumerate(para_items):
line_h = heights[idx]
if y_cur > b_px + 4:
break
if not txt.strip():
y_cur += line_h
continue
# estimate chars-per-line from font
try:
ch_w = font.getlength('m') if hasattr(font, 'getlength') else 7
except Exception:
ch_w = 7
margin = 4
avail_w = max(20, w - margin * 2)
cpl = max(5, int(avail_w / max(1, ch_w)))
lines = textwrap.wrap(txt, width=cpl) or [txt]
for line in lines:
if y_cur > b_px + 4:
break
try:
lw = font.getlength(line) if hasattr(font, 'getlength') else len(line) * ch_w
except Exception:
lw = len(line) * ch_w
if align == PP_ALIGN.CENTER:
x = l + max(0, (w - lw) // 2)
elif align == PP_ALIGN.RIGHT:
x = l + max(0, w - int(lw) - margin)
else:
x = l + margin
draw.text((x, y_cur), line, font=font, fill=color)
y_cur += line_h
except Exception as e:
pass # silently skip broken shapes
return img
# ── render all slides ─────────────────────────────────────────────────────────
slides_imgs = []
for i, slide in enumerate(prs.slides):
img = draw_slide(slide)
slides_imgs.append(img)
print(f" Slide {i+1} rendered")
# ── stitch ────────────────────────────────────────────────────────────────────
SEP = 8
TOTAL_H = SLIDE_H * len(slides_imgs) + SEP * (len(slides_imgs) - 1)
combined = Image.new('RGB', (SLIDE_W, TOTAL_H), (20, 20, 30))
y = 0
for img in slides_imgs:
combined.paste(img, (0, y))
y += SLIDE_H + SEP
combined.save(OUT_PATH, 'PNG')
print(f"\nSaved: {OUT_PATH} ({combined.width}x{combined.height})")
python3 /tmp/workspace/root-canal-pptx/render_slides2.py 2>&1 && python3 -c " from PIL import Image img = Image.open('/tmp/workspace/root-canal-pptx/all_slides.png') # sample pixels across different areas for y in [50, 200, 400, 800, 1600, 2400]: px = img.getpixel((700, y)) print(f' y={y}: {px}') "
python3 -c " from pptx import Presentation from pptx.enum.dml import MSO_FILL from pptx.oxml.ns import qn prs = Presentation('/tmp/workspace/attachments/958287a7-9624-4643-a175-6fec30cb352d/BioRoot_Flow_CSBS_Presentation (1).pptx') slide = prs.slides[0] shapes = list(slide.shapes) # test shape 1 (Rectangle 2 - should be MED_BLUE 2E6DA4) s = shapes[1] fill = s.fill print('fill.type:', fill.type) print('MSO_FILL.SOLID:', MSO_FILL.SOLID) print('Are equal:', fill.type == MSO_FILL.SOLID) print('fill.type value:', fill.type.value if hasattr(fill.type, 'value') else fill.type) print('MSO_FILL.SOLID value:', MSO_FILL.SOLID.value if hasattr(MSO_FILL.SOLID, 'value') else MSO_FILL.SOLID) # try getting rgb directly try: rgb = fill.fore_color.rgb print('rgb:', rgb, 'R:', rgb.red, 'G:', rgb.green, 'B:', rgb.blue) except Exception as e: print('rgb error:', e) "
python3 -c " from pptx import Presentation prs = Presentation('/tmp/workspace/attachments/958287a7-9624-4643-a175-6fec30cb352d/BioRoot_Flow_CSBS_Presentation (1).pptx') slide = prs.slides[0] shapes = list(slide.shapes) s = shapes[1] rgb = s.fill.fore_color.rgb print(type(rgb), dir(rgb)) print(repr(rgb)) print(str(rgb)) print(int(rgb)) "
~/root-canal-pptx/render_slides3.py
"""
Render each PPTX slide to PIL Image, then stack into one tall PNG.
RGBColor is a tuple: rgb[0]=R, rgb[1]=G, rgb[2]=B
"""
from pptx import Presentation
from pptx.enum.dml import MSO_FILL
from pptx.enum.text import PP_ALIGN
from PIL import Image, ImageDraw, ImageFont
import textwrap, os
PPTX_PATH = '/tmp/workspace/attachments/958287a7-9624-4643-a175-6fec30cb352d/BioRoot_Flow_CSBS_Presentation (1).pptx'
OUT_PATH = '/tmp/workspace/root-canal-pptx/all_slides.png'
prs = Presentation(PPTX_PATH)
SW_emu = prs.slide_width
SH_emu = prs.slide_height
TARGET_W = 1400
SCALE = TARGET_W / SW_emu
SLIDE_W = int(SW_emu * SCALE)
SLIDE_H = int(SH_emu * SCALE)
print(f"Slide px: {SLIDE_W} x {SLIDE_H}")
FONT_DIR = '/usr/share/fonts/truetype/dejavu/'
_FONTS = {}
def get_font(size_pt, bold=False, italic=False):
key = (round(size_pt, 1), bold, italic)
if key in _FONTS:
return _FONTS[key]
# Convert pt → pixels at our canvas scale
px = max(9, int(size_pt * 12700 * SCALE))
if bold and italic:
f_file = FONT_DIR + 'DejaVuSans-BoldOblique.ttf'
elif bold:
f_file = FONT_DIR + 'DejaVuSans-Bold.ttf'
elif italic:
f_file = FONT_DIR + 'DejaVuSans-Oblique.ttf'
else:
f_file = FONT_DIR + 'DejaVuSans.ttf'
if not os.path.exists(f_file):
f_file = FONT_DIR + 'DejaVuSans.ttf'
try:
f = ImageFont.truetype(f_file, px)
except Exception:
f = ImageFont.load_default()
_FONTS[key] = f
return f
def ep(emu):
return int(emu * SCALE)
def get_fill_rgb(shape):
"""Return (R,G,B) tuple or None."""
try:
fill = shape.fill
if fill.type == MSO_FILL.SOLID:
rgb = fill.fore_color.rgb # RGBColor is a tuple
return (rgb[0], rgb[1], rgb[2])
except Exception:
pass
return None
def get_run_font_color(run):
try:
rgb = run.font.color.rgb
return (rgb[0], rgb[1], rgb[2])
except Exception:
return None
def draw_slide(slide_obj):
img = Image.new('RGB', (SLIDE_W, SLIDE_H), (244, 246, 249))
draw = ImageDraw.Draw(img)
for shape in slide_obj.shapes:
try:
l = ep(shape.left or 0)
t = ep(shape.top or 0)
w = ep(shape.width or 0)
h = ep(shape.height or 0)
# ── filled rectangle ───────────────────────────────────────────
fill_c = get_fill_rgb(shape)
if fill_c and w > 0 and h > 0:
draw.rectangle([l, t, l+w, t+h], fill=fill_c)
# ── text ───────────────────────────────────────────────────────
if not shape.has_text_frame:
continue
tf = shape.text_frame
try:
from pptx.enum.text import MSO_ANCHOR
va = tf.vertical_anchor
v_middle = (va == MSO_ANCHOR.MIDDLE)
v_bottom = (va == MSO_ANCHOR.BOTTOM)
except Exception:
v_middle = v_bottom = False
# parse paragraphs
para_data = [] # (text, font, color, align)
for para in tf.paragraphs:
try:
align = para.alignment or PP_ALIGN.LEFT
except Exception:
align = PP_ALIGN.LEFT
run_texts = []
sz_pt = 12.0
bold = False
italic = False
color = (51, 63, 74)
for run in para.runs:
rt = run.text
if not rt:
continue
run_texts.append(rt)
rf = run.font
if rf.size:
sz_pt = rf.size / 12700.0
if rf.bold is not None: bold = rf.bold
if rf.italic is not None: italic = rf.italic
rc = get_run_font_color(run)
if rc: color = rc
txt = ''.join(run_texts)
font = get_font(sz_pt, bold, italic)
para_data.append((txt, font, color, align, sz_pt))
if not para_data:
continue
def line_height(font, sz_pt):
try:
bb = font.getbbox('Ag')
return bb[3] - bb[1] + max(2, int(sz_pt * 12700 * SCALE * 0.3))
except Exception:
return max(12, int(sz_pt * 12700 * SCALE * 1.3))
lheights = [line_height(f, sz) for _, f, _, _, sz in para_data]
total_h = sum(lheights)
if v_middle:
y = t + max(0, (h - total_h) // 2)
elif v_bottom:
y = t + max(0, h - total_h - 4)
else:
y = t + 3
for i, (txt, font, color, align, sz_pt) in enumerate(para_data):
lh = lheights[i]
if y > t + h + 6:
break
if not txt.strip():
y += lh
continue
try:
ch_w = font.getlength('m') if hasattr(font, 'getlength') else 7
except Exception:
ch_w = 7
margin = 4
avail_w = max(20, w - margin * 2)
cpl = max(5, int(avail_w / max(1.0, ch_w)))
lines = textwrap.wrap(txt, width=cpl) or [txt]
for line in lines:
if y > t + h + 6:
break
try:
lw = font.getlength(line) if hasattr(font, 'getlength') else len(line)*ch_w
except Exception:
lw = len(line) * ch_w
if align == PP_ALIGN.CENTER:
x = l + max(0, int((w - lw) // 2))
elif align == PP_ALIGN.RIGHT:
x = l + max(0, w - int(lw) - margin)
else:
x = l + margin
draw.text((x, y), line, font=font, fill=color)
y += lh
except Exception:
pass
return img
# ── render ────────────────────────────────────────────────────────────────────
imgs = []
for i, slide in enumerate(prs.slides):
img = draw_slide(slide)
imgs.append(img)
# verify non-blank
px = img.getpixel((10, 10))
print(f" Slide {i+1} done sample_px={px}")
# ── stitch ─────────────────────────────────────────────────────────────────────
SEP = 8
total_h = SLIDE_H * len(imgs) + SEP * (len(imgs)-1)
combined = Image.new('RGB', (SLIDE_W, total_h), (15, 20, 35))
y = 0
for img in imgs:
combined.paste(img, (0, y))
y += SLIDE_H + SEP
combined.save(OUT_PATH, 'PNG')
print(f"\nSaved: {OUT_PATH} size={combined.size}")
python3 /tmp/workspace/root-canal-pptx/render_slides3.py 2>&1 && python3 -c " from PIL import Image img = Image.open('/tmp/workspace/root-canal-pptx/all_slides.png') for y in [30, 100, 300, 600, 790, 900, 1600, 2400, 3200]: print(f'y={y}: {img.getpixel((700,y))}') "
python3 -c " from PIL import Image img = Image.open('/tmp/workspace/root-canal-pptx/all_slides.png') # Check text area - should NOT be pure dark blue if text is drawn over it # title area slide 1 is around y=40-130 (dark header ~140px tall) # text boxes are over the colored rectangles so text pixels will be white # Let's sample many points import collections slide_h = 787 for slide_i in range(5): base_y = slide_i * (787 + 8) # header area hdr_px = img.getpixel((400, base_y + 30)) # body area body_px = img.getpixel((400, base_y + 400)) print(f'Slide {slide_i+1}: header_px={hdr_px}, body_px={body_px}') print('Image size:', img.size) " 2>&1
all slides
Image · PNG