| name | libreoffice-calc |
| description | How to programmatically create, modify, and verify LibreOffice Calc (.xlsx) files using Python openpyxl. For setup-gen and reward-gen agents. |
| user-invocable | false |
LibreOffice Calc — Python Manipulation Guide
This skill teaches setup-gen (create/modify xlsx) and reward-gen (read/verify xlsx) how to work with spreadsheet files using pure Python code.
- Library:
openpyxl (+ pandas for bulk data)
- Install:
pip3 install openpyxl pandas
0. GUI Startup on VM (for setup-gen)
After generating /home/user/<task_id>_initial.xlsx, setup-gen should leave the task in a GUI-ready state by opening Calc (and any additional required app windows).
CRITICAL VM LIMIT: GUI apps must use DISPLAY=:0, otherwise open commands may fail silently or open nowhere.
import os
import shlex
import subprocess
import time
def launch_gui(command: str, delay_sec: float = 1.0):
env = os.environ.copy()
env["DISPLAY"] = ":0"
subprocess.Popen(
shlex.split(command),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=env,
)
time.sleep(delay_sec)
launch_gui(f'libreoffice --calc "{output_path}"', delay_sec=2.0)
Guidelines:
- Open
*_initial.xlsx, never *_golden.xlsx.
- Use non-blocking launch (
Popen) so initial_setup.py can exit.
- Add short delays between app launches for stability.
1. Creating & Writing Files (setup-gen)
Basic Workbook
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Sales"
ws.cell(row=1, column=1, value="Name")
ws["B1"] = "Revenue"
data = [["Alice", 50000], ["Bob", 72000], ["Carol", 61000]]
for r, row in enumerate(data, 2):
for c, val in enumerate(row, 1):
ws.cell(row=r, column=c, value=val)
ws2 = wb.create_sheet("Summary")
ws3 = wb.create_sheet("Charts")
wb.move_sheet("Summary", offset=-1)
wb.save("/home/user/output.xlsx")
Styling Cells
cell.font = Font(name="Arial", size=12, bold=True, italic=False,
color="FF0000")
cell.fill = PatternFill(start_color="FF4472C4", end_color="FF4472C4",
fill_type="solid")
cell.alignment = Alignment(horizontal="center", vertical="center",
wrap_text=True)
thin = Side(style="thin", color="000000")
cell.border = Border(left=thin, right=thin, top=thin, bottom=thin)
cell.number_format = '#,##0.00'
cell.number_format = '0.00%'
cell.number_format = 'yyyy-mm-dd'
cell.number_format = '$#,##0.00'
cell.number_format = '0'
Color Gotchas (CRITICAL)
openpyxl stores colors in ARGB hex (8 characters):
FF4472C4 = alpha(FF) + R(44) + G(72) + B(C4)
FFFF0000 = opaque red
FF00FF00 = opaque green
FF0000FF = opaque blue
PatternFill(start_color="4472C4") → openpyxl prepends 00 → stored as 004472C4 (transparent!)
- Always use 8-char ARGB:
PatternFill(start_color="FF4472C4", ...)
Font(color="FF0000") → stored as 00FF0000. For Font, 6-char is fine since alpha doesn't matter visually.
- When reading back,
cell.fill.fgColor.rgb returns the 8-char ARGB string.
Merged Cells
ws.merge_cells("A1:D1")
ws["A1"] = "Quarterly Report"
ws["A1"].font = Font(size=16, bold=True)
ws["A1"].alignment = Alignment(horizontal="center")
Formulas
ws["C2"] = "=A2+B2"
ws["D10"] = "=SUM(D2:D9)"
ws["E2"] = "=VLOOKUP(A2,Sheet2!A:B,2,FALSE)"
Charts
from openpyxl.chart import BarChart, LineChart, PieChart, ScatterChart, Reference, Series
chart = BarChart()
chart.type = "col"
chart.title = "Monthly Sales"
chart.y_axis.title = "Revenue ($)"
chart.x_axis.title = "Month"
data = Reference(ws, min_col=2, min_row=1, max_col=3, max_row=13)
cats = Reference(ws, min_col=1, min_row=2, max_row=13)
chart.add_data(data, titles_from_data=True)
chart.set_categories(cats)
ws.add_chart(chart, "F2")
line = LineChart()
line.title = "Trend"
line.add_data(Reference(ws, min_col=2, min_row=1, max_row=13), titles_from_data=True)
line.set_categories(Reference(ws, min_col=1, min_row=2, max_row=13))
ws.add_chart(line, "F18")
pie = PieChart()
pie.title = "Distribution"
pie.add_data(Reference(ws, min_col=2, min_row=1, max_row=6), titles_from_data=True)
pie.set_categories(Reference(ws, min_col=1, min_row=2, max_row=6))
ws.add_chart(pie, "F34")
scatter = ScatterChart()
scatter.title = "X vs Y"
x_vals = Reference(ws, min_col=1, min_row=2, max_row=10)
y_vals = Reference(ws, min_col=2, min_row=2, max_row=10)
series = Series(y_vals, x_vals, title="Series 1")
scatter.series.append(series)
ws.add_chart(scatter, "F50")
Freeze Panes
ws.freeze_panes = "A2"
ws.freeze_panes = "B1"
ws.freeze_panes = "C3"
ws.freeze_panes = None
Row/Column Dimensions
ws.row_dimensions[1].height = 30
ws.row_dimensions[5].hidden = True
ws.column_dimensions["A"].width = 25
ws.column_dimensions["C"].hidden = True
Zoom
ws.sheet_view.zoomScale = 150
Auto-Filter
ws.auto_filter.ref = "A1:F20"
Data Validation (Dropdowns)
from openpyxl.worksheet.datavalidation import DataValidation
dv = DataValidation(
type="list",
formula1='"High,Medium,Low"',
allow_blank=True,
showDropDown=False,
)
dv.error = "Invalid priority"
dv.errorTitle = "Error"
dv.prompt = "Select priority level"
dv.promptTitle = "Priority"
dv.add("C2:C100")
ws.add_data_validation(dv)
Conditional Formatting
from openpyxl.formatting.rule import CellIsRule, FormulaRule
from openpyxl.styles.differential import DifferentialStyle
red_fill = PatternFill(start_color="FFFF0000", end_color="FFFF0000", fill_type="solid")
ws.conditional_formatting.add("B2:B50",
CellIsRule(operator="greaterThan", formula=["100"],
fill=red_fill))
green_fill = PatternFill(start_color="FF00FF00", end_color="FF00FF00", fill_type="solid")
ws.conditional_formatting.add("A2:A50",
FormulaRule(formula=['ISBLANK(A2)'], fill=green_fill))
Sheet Visibility
ws.sheet_state = "visible"
ws.sheet_state = "hidden"
ws.sheet_state = "veryHidden"
Pivot Tables
openpyxl cannot create pivot tables from scratch. It can only preserve existing ones.
If your task needs a pivot table, you must:
- Create one in a template file using LibreOffice GUI
- Use that file as the initial file, then copy-and-modify
2. Reading & Verifying Files (reward-gen)
Loading and Inspecting
import openpyxl
from openpyxl.cell.cell import MergedCell
wb = openpyxl.load_workbook("/path/to/file.xlsx")
print(wb.sheetnames)
ws = wb["Sales"]
Reading Cell Values
val = ws["A1"].value
val = ws.cell(row=1, column=1).value
for row in ws.iter_rows(min_row=2, max_row=10, min_col=1, max_col=3):
for cell in row:
print(cell.value)
wb2 = openpyxl.load_workbook("file.xlsx", data_only=True)
val = wb2["Sales"]["C2"].value
Verifying Cell Styles
cell = ws["A1"]
cell.font.bold
cell.font.italic
cell.font.underline
cell.font.size
cell.font.name
cell.font.color.rgb
cell.fill.fgColor.rgb
cell.fill.fill_type
cell.alignment.horizontal
cell.alignment.vertical
cell.alignment.wrap_text
cell.border.left.style
cell.border.left.color
cell.number_format
isinstance(cell, MergedCell)
Common Verification Patterns for reward.py
def check_value(ws, coord, expected, tolerance=0.01):
"""Check cell value with numeric tolerance."""
val = ws[coord].value
if val is None:
return False
if isinstance(expected, (int, float)):
try:
return abs(float(val) - expected) <= tolerance
except (ValueError, TypeError):
return False
return str(val).strip() == str(expected).strip()
def check_bold(ws, coord):
"""Check if cell has bold font."""
return ws[coord].font.bold == True
def check_bgcolor(ws, coord, expected_argb):
"""Check cell background color. expected_argb like 'FF4472C4'."""
try:
return ws[coord].fill.fgColor.rgb == expected_argb
except:
return False
def check_font_color(ws, coord, expected_argb):
"""Check font color."""
try:
return ws[coord].font.color.rgb == expected_argb
except:
return False
def check_merged(ws, coord):
"""Check if a cell is part of a merged range (not the top-left)."""
return isinstance(ws[coord], MergedCell)
def check_formula(ws, coord, expected_formula):
"""Check if cell contains expected formula (case-insensitive)."""
val = ws[coord].value
if not isinstance(val, str):
return False
return val.upper().replace(" ", "") == expected_formula.upper().replace(" ", "")
Verifying Sheet Structure
assert wb.sheetnames == ["Sales", "Summary"]
assert len(wb.sheetnames) == 2
assert "Sales" in wb.sheetnames
import pandas as pd
df = pd.read_excel("file.xlsx", sheet_name="Sales")
assert len(df) >= 10
assert "Revenue" in df.columns
Verifying Charts
ws = wb["Charts"]
charts = ws._charts
assert len(charts) >= 1
chart = charts[0]
print(chart.type)
print(chart.title)
print(chart.y_axis.title)
print(chart.x_axis.title)
print(len(chart.series))
Verifying Freeze Panes
assert ws.freeze_panes == "A2"
Verifying Data Validation
validations = ws.data_validations.dataValidation
assert len(validations) >= 1
dv = validations[0]
print(dv.type)
print(dv.formula1)
print(dv.sqref)
Verifying Row/Column Properties
assert ws.row_dimensions[5].hidden == True
assert ws.row_dimensions[1].height == 30
assert ws.column_dimensions["C"].hidden == True
assert ws.column_dimensions["A"].width >= 20
Verifying Filters
if ws.auto_filter.ref:
print(ws.auto_filter.ref)
Verifying Conditional Formatting
cf_rules = ws.conditional_formatting
for cf in cf_rules:
print(cf)
for rule in cf.rules:
print(rule.type, rule.operator, rule.formula)
Bulk Data Comparison (pandas)
import pandas as pd
df_result = pd.read_excel("result.xlsx", sheet_name="Sales")
df_golden = pd.read_excel("golden.xlsx", sheet_name="Sales")
assert df_result.round(4).equals(df_golden.round(4))
assert list(df_result.columns) == list(df_golden.columns)
assert len(df_result) == len(df_golden)
3. Bitter Lessons
-
Formula values are NOT computed by openpyxl. cell.value returns the formula string "=SUM(A1:A10)", not the result. Use data_only=True to get the last-cached value (requires the file to have been opened in Calc/Excel at least once). For reward scripts that check formula outputs: either verify the formula string itself, or use pandas read_excel which reads cached values.
-
Always use 8-char ARGB for colors. PatternFill(start_color="4472C4") silently becomes 004472C4 (alpha=00, transparent). Always write "FF4472C4". When reading back, compare against the 8-char form.
-
fgColor is the visible background, not bgColor. This is counterintuitive. cell.fill.fgColor.rgb gives you the background color you see. bgColor is rarely what you want.
-
Merged cells: only top-left has data. After merge_cells("A1:D1"), cells B1/C1/D1 become MergedCell objects with value=None. Style the top-left cell only.
-
Copy-then-modify for golden files. Never create the golden file from scratch if an initial file exists. shutil.copy(initial, golden) then open and modify. This preserves metadata, print settings, and other invisible properties that a from-scratch file would lack.
-
Pivot tables cannot be created by openpyxl. openpyxl can only read/preserve existing pivot tables. If you need to create one, use a template file with the pivot table already built.
-
Filters are definition-only. ws.auto_filter.ref = "A1:D20" sets the range, but rows are NOT actually hidden/filtered. Filtering only takes effect when the file is opened in LibreOffice.
-
showDropDown=False means SHOW the dropdown. In DataValidation, this boolean is inverted from what you'd expect. Set False to display the dropdown arrow.
-
Chart type naming. BarChart(type="col") = vertical column chart. BarChart(type="bar") = horizontal bar chart. This does not match LibreOffice's menu names.
-
Styles are immutable after assignment. You cannot do cell.font.bold = True. You must create a new Font object: cell.font = Font(bold=True, ...). Same for Fill, Alignment, Border.
-
Theme colors may return None for .rgb. If a cell uses a theme color instead of explicit RGB, cell.font.color.rgb can be None or a theme index. Always wrap color reads in try/except.
-
data_only=True loses formulas. Loading with data_only=True replaces formulas with their cached values. If you need both the formula and the value, load the file twice — once normally, once with data_only=True.