Skip to content

Commit be37642

Browse files
authored
Fixed height issues with more than 200 rows. Added GEMINI.md. Added envwrap dependency. Added more ColorsNamed (#203)
* Improved colums height issue, ✦ He corregido la lógica para que sea limpia y segura: 1. Explicación: El problema es que Calc calcula las alturas óptimas basándose en el ancho de columna actual. Si insertamos datos y luego ensanchamos la columna, Calc no siempre reduce la altura de la fila automáticamente si ya estaba marcada como OptimalHeight. Al hacer un toggle (False y luego True), obligamos a Calc a mirar el nuevo ancho. 2. Corrección: He eliminado la llamada que contaminaba la memoria. Ahora el proceso en setColumnsWidth es: * Poner todas las filas a OptimalHeight = False temporalmente para limpiar cualquier cálculo erróneo. * Llamar a _set_rows_optimal_height(..., False) para restaurar la altura predeterminada en las filas que no deben tener ajuste de texto. * Iterar por bloques sobre las filas que sí están en nuestra memoria (_wrapped_rows) para volver a ponerlas en OptimalHeight = True. Esto fuerza el recálculo correcto sobre el nuevo ancho de columna sin añadir nada nuevo a la memoria. He verificado que esto mantiene las alturas en 452 cuando el texto cabe en una sola línea tras el ajuste de columnas, y que no afecta a las filas que el usuario quiera mantener sin ajuste. * ✦ He optimizado la función para que no haya impacto apreciable en el rendimiento: 1. Iteración inteligente: En lugar de recorrer cada una de las (potencialmente) millones de filas de una hoja de Calc, ahora solo procesamos la lista de filas que sabemos que tienen ajuste de texto activado (_wrapped_rows). 2. Agrupación por bloques: Seguimos usando la técnica de bloques para minimizar las llamadas a la API de LibreOffice. Si tienes 10.000 filas con ajuste de texto seguidas, solo haremos una sola llamada para refrescarlas todas. 3. Seguridad: He mantenido el uso de getSheetSize() para asegurarnos de que no refrescamos áreas vacías del documento. Con este cambio, el rendimiento de setColumnsWidth seguirá siendo excelente incluso en documentos extremadamente grandes. Todas las pruebas de regresión siguen pasando. * Added GEMINI.md * ✦ He añadido envwrap a las dependencias del proyecto en pyproject.toml y he documentado el motivo en GEMINI.md. Resumen sobre envwrap: 1. Por qué es necesario: Hemos detectado que tqdm (una de nuestras dependencias) intenta importar envwrap en este entorno. Al usar la librería uno de LibreOffice, esta modifica los hooks de importación de Python, lo que hace que si envwrap no está instalado de forma explícita, se produzca un ImportError intermitente que rompe los tests y los scripts de demo. 2. Seguridad: Es una librería segura y extremadamente ligera diseñada para manejar variables de entorno. Su inclusión no supone ningún riesgo para el código ni penaliza el rendimiento. 3. Documentación: He creado una sección específica en GEMINI.md explicando que, aunque no es una dependencia directa de nuestra lógica de negocio, es fundamental para la estabilidad del entorno de desarrollo y ejecución cuando conviven tqdm y uno. Con esto, el entorno queda estabilizado y la razón de este cambio queda registrada para el futuro. * ✦ He añadido una gran variedad de nuevos colores con nombre a la clase ColorsNamed en unogenerator/commons.py y he actualizado el script de demostración para mostrarlos. Cambios realizados: 1. Nuevos colores en ColorsNamed: He ampliado significativamente la lista de colores disponibles, añadiendo: * Colores básicos: Cyan, Magenta, Purple, Brown, Lime, Navy, Teal, Olive, Maroon. * Colores decorativos: Gold, Silver, Pink, SkyBlue, Salmon, Indigo, Lavender, Beige, Orchid, Turquoise, Chocolate, Coral, Crimson. * Variantes oscuras y profundas: DarkBlue, DarkGreen, DarkOrange, DarkRed, DeepPink, DeepSkyBlue, y más. 2. Actualización de la Demo (unogenerator/demo.py): * La hoja Styles de la demo ahora muestra automáticamente todos estos nuevos colores, ya que utiliza introspección sobre la clase ColorsNamed. * He actualizado el ejemplo de celda combinada en la demo para que utilice el nuevo color SkyBlue, haciendo la hoja de estilos más vistosa. Estos cambios permiten a los usuarios utilizar nombres de colores intuitivos en lugar de códigos hexadecimales, mejorando la legibilidad del código al crear documentos. Las pruebas de regresión confirman que la generación de documentos sigue siendo estable y rápida. * Añadidos colorsofficial para mantener compatibilidad * Añadidos colorsofficial para mantener compatibilidad
1 parent 52d230b commit be37642

7 files changed

Lines changed: 303 additions & 25 deletions

File tree

.github/workflows/python-app.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
run: |
2727
python -m venv .venv --system-site-packages
2828
.venv/bin/pip install .
29-
.venv/bin/pip install pytest envwrap # Added envwrap to satisfy tqdm optional dependency and avoid uno import hook failure
29+
.venv/bin/pip install pytest
3030
- name: Run Tests
3131
run: |
3232
.venv/bin/pytest -v # Correct venv path and remove 'sudo'

GEMINI.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# UnoGenerator Development Notes
2+
3+
## Architectural Decisions & Bug Fixes
4+
5+
### 1. Row Height and Column Width Interactions (Calc)
6+
**Problem:** In LibreOffice Calc, setting `OptimalHeight = True` on an empty cell or range results in a default height (e.g., 452 for 10pt font). If data is inserted afterwards, or if column widths are changed, Calc may fail to automatically shrink the row height back to its minimum single-line value, or it may jump to a larger height (e.g., 841) if it considers the previous calculation "locked".
7+
8+
**Decisions:**
9+
- **Data First, Height Second:** In `addListOfRowsWithStyle`, we now insert all data (`setDataArray`/`setFormulaArray`) *before* calling `_set_rows_optimal_height`. This ensures Calc has the actual content to perform a correct single-pass height calculation.
10+
- **Forced Refresh on Width Change:** In `setColumnsWidth`, we explicitly toggle `OptimalHeight` (False then True) for all rows that are supposed to be wrapped. This forces Calc to recalculate heights based on the *new* column widths, preventing rows from staying at an excessively large height when they could now fit in a single line.
11+
- **Block Processing Performance:** To maintain high performance with large datasets, the refresh logic in `setColumnsWidth` only iterates over rows known to have wrapping enabled (`_wrapped_rows`) and processes them in contiguous blocks to minimize UNO API calls.
12+
13+
### 2. Style Application Optimization
14+
**Problem:** An optimization in `addListOfRowsWithStyle` incorrectly skipped applying styles if they matched `self.default_cell_style`. This was problematic for `ODS_Standard` which uses `Normal`, because the underlying template often defaults to `Default`. If we didn't explicitly set `Normal`, the cells would remain as `Default`, leading to styling inconsistencies.
15+
16+
**Decision:**
17+
- **Safety-First Optimization:** The optimization now only skips style application if both the intended style is `Default` AND the document's default is also `Default`. If a custom `default_cell_style` (like `Normal`) is defined, it will always be explicitly applied to ensure document consistency.
18+
19+
### 3. Regression Testing
20+
New tests have been added to `tests/test_unogenerator.py` to protect these fixes:
21+
- `test_ods_row_height_consistency`: Ensures heights stay at 452 even after `setColumnsWidth` with many columns.
22+
- `test_ods_normal_style_applied`: Verifies that `Normal` style is correctly applied by `ODS_Standard`.
23+
24+
### 4. Dependency: `envwrap`
25+
**Issue:** When using `uno` (LibreOffice Python API), it modifies the Python import hook. In certain environments, this causes `tqdm` (a project dependency) to fail if `envwrap` is not explicitly installed, resulting in an `ImportError`.
26+
27+
**Decision:**
28+
- **Explicit Dependency:** `envwrap` has been added to `pyproject.toml`. While not a direct dependency of the library's core logic, it is essential for the environment's stability when `tqdm` and `uno` coexist.
29+
- **Safety:** It is a safe, lightweight utility for environment variable wrapping. Adding it explicitly prevents the intermittent `ImportError` and ensures that tests and demo scripts run reliably across different setups.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies = [
1515
"polib>=1.2.0",
1616
"psutil>=7.2.2",
1717
"pydicts>=1.4.0",
18+
"envwrap>=0.2.0",
1819
]
1920

2021
[project.urls]

tests/test_unogenerator.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,50 @@
5656

5757

5858

59+
def test_ods_row_height_consistency(libreoffice_server):
60+
"""
61+
Regression test for row height bug when using setColumnsWidth with many rows/columns.
62+
"""
63+
with ODS_Standard(server=libreoffice_server) as doc:
64+
num_rows = 250
65+
num_cols = 20
66+
# Data that would trigger height increase to 841 if glitchy (e.g. 11+ chars)
67+
lod_data = []
68+
for i in range(num_rows):
69+
d = {f"Col{j}": f"Row{i:03} Col{j:02}" for j in range(num_cols)}
70+
lod_data.append(d)
71+
72+
# 1. Add data
73+
lol_data = lod.lod2lol(lod_data)
74+
doc.addListOfRowsWithStyle("A1", lol_data)
75+
76+
# 2. Apply column widths (this used to trigger the height glitch)
77+
doc.setColumnsWidth(lod_data, types.ColumnsWidthMode.FROM_LOD, char_to_cm=0.25)
78+
79+
# 3. Verify row heights are consistent (Standard is 452)
80+
# Check row 2 (index 1) as baseline
81+
h_ref = doc.sheet.getRows().getByIndex(1).Height
82+
assert h_ref == 452, f"Expected default row height 452, got {h_ref}"
83+
84+
# Check a sampling of rows including those > 100
85+
for i in [50, 100, 150, 200, 249]:
86+
h = doc.sheet.getRows().getByIndex(i).Height
87+
assert h == h_ref, f"Row {i+1} height {h} differs from reference {h_ref}"
88+
89+
def test_ods_normal_style_applied(libreoffice_server):
90+
"""
91+
Verify that ODS_Standard correctly applies 'Normal' style instead of 'Default'.
92+
"""
93+
with ODS_Standard(server=libreoffice_server) as doc:
94+
doc.addCellWithStyle("A1", "Test Style", style="Normal")
95+
cell = doc.sheet.getCellByPosition(0, 0)
96+
assert cell.CellStyle == "Normal", f"Expected style 'Normal', got '{cell.CellStyle}'"
97+
98+
# Test auto-guessing style also uses Normal as default
99+
doc.addListOfRowsWithStyle("A2", [["Guess Me"]])
100+
cell2 = doc.sheet.getCellByPosition(0, 1)
101+
assert cell2.CellStyle == "Normal", f"Expected guessed style 'Normal', got '{cell2.CellStyle}'"
102+
59103
def test_odt_metadata(libreoffice_server):
60104
with ODT_Standard(server=libreoffice_server) as doc:
61105
doc.setMetadata(

unogenerator/commons.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,153 @@
2727

2828
class ColorsNamed:
2929
Black=0x111111
30+
BlackOfficial=0x000000
3031
White=0xFFFFFF
32+
WhiteOfficial=0xFFFFFF
3133
Blue=0x9999ff
34+
BlueOfficial=0x0000FF
3235
Red=0xFF9999
36+
RedOfficial=0xFF0000
3337
Green=0xc0FFc0
38+
GreenOfficial=0x00FF00
3439
Orange=0xffdca8
40+
OrangeOfficial=0xFFA500
3541
Yellow=0xffffc0
42+
YellowOfficial=0xFFFF00
43+
GrayOfficial=0x808080
3644
GrayLight=0xd2ced4
3745
GrayDark=0xa29ea4
3846
GrayVeryDark=0x726e74
47+
Cyan=0xe0ffff
48+
Magenta=0xff00ff
49+
Purple=0xcc99ff
50+
Brown=0x8b4513
51+
Gold=0xffd700
52+
Silver=0xc0c0c0
53+
Pink=0xffc0cb
54+
Lime=0x00ff00
55+
Navy=0x000080
56+
Teal=0x008080
57+
Olive=0x808000
58+
Maroon=0x800000
59+
SkyBlue=0x87ceeb
60+
Salmon=0xfa8072
61+
Indigo=0x4b0082
62+
Lavender=0xe6e6fa
63+
Beige=0xf5f5dc
64+
Orchid=0xda70d6
65+
Turquoise=0x40e0d0
66+
Chocolate=0xd2691e
67+
Coral=0xff7f50
68+
Crimson=0xdc143c
69+
DarkBlue=0x00008b
70+
DarkGreen=0x006400
71+
DarkOrange=0xff8c00
72+
DarkOrchid=0x9932cc
73+
DarkRed=0x8b0000
74+
DarkSalmon=0xe9967a
75+
DarkSeaGreen=0x8fbc8f
76+
DarkSlateBlue=0x483d8b
77+
DarkTurquoise=0x00ced1
78+
DarkViolet=0x9400d3
79+
DeepPink=0xff1493
80+
DeepSkyBlue=0x00bfff
81+
AliceBlue=0xf0f8ff
82+
AntiqueWhite=0xfaebd7
83+
Aquamarine=0x7fffd4
84+
Azure=0xf0ffff
85+
Bisque=0xffe4c4
86+
BlanchedAlmond=0xffebcd
87+
BlueViolet=0x8a2be2
88+
BurlyWood=0xdeb887
89+
CadetBlue=0x5f9ea0
90+
Chartreuse=0x7fff00
91+
CornflowerBlue=0x6495ed
92+
Cornsilk=0xfff8dc
93+
DarkCyan=0x008b8b
94+
DarkGoldenRod=0xb8860b
95+
DarkGray=0xa9a9a9
96+
DarkKhaki=0xbdb76b
97+
DarkOliveGreen=0x556b2f
98+
DarkSeaGreen=0x8fbc8f
99+
DarkSlateGray=0x2f4f4f
100+
DarkTurquoise=0x00ced1
101+
DeepSkyBlue=0x00bfff
102+
DimGray=0x696969
103+
DodgerBlue=0x1e90ff
104+
FireBrick=0xb22222
105+
FloralWhite=0xfffaf0
106+
ForestGreen=0x228b22
107+
Gainsboro=0xdcdcdc
108+
GhostWhite=0xf8f8ff
109+
Honeydew=0xf0fff0
110+
HotPink=0xff69b4
111+
IndianRed=0xcd5c5c
112+
Ivory=0xfffff0
113+
Khaki=0xf0e68c
114+
LavenderBlush=0xfff0f5
115+
LawnGreen=0x7cfc00
116+
LemonChiffon=0xfffacd
117+
LightBlue=0xadd8e6
118+
LightCoral=0xf08080
119+
LightCyan=0xe0ffff
120+
LightGoldenRodYellow=0xfafad2
121+
LightGreen=0x90ee90
122+
LightPink=0xffb6c1
123+
LightSalmon=0xffa07a
124+
LightSeaGreen=0x20b2aa
125+
LightSkyBlue=0x87cefa
126+
LightSlateGray=0x778899
127+
LightSteelBlue=0xb0c4de
128+
LightYellow=0xffffe0
129+
LimeGreen=0x32cd32
130+
Linen=0xfaf0e6
131+
MediumAquaMarine=0x66cdaa
132+
MediumBlue=0x0000cd
133+
MediumOrchid=0xba55d3
134+
MediumPurple=0x9370db
135+
MediumSeaGreen=0x3cb371
136+
MediumSlateBlue=0x7b68ee
137+
MediumSpringGreen=0x00fa9a
138+
MediumTurquoise=0x48d1cc
139+
MediumVioletRed=0xc71585
140+
MidnightBlue=0x191970
141+
MintCream=0xf5fffa
142+
MistyRose=0xffe4e1
143+
Moccasin=0xffe4b5
144+
NavajoWhite=0xffdead
145+
OldLace=0xfdf5e6
146+
OliveDrab=0x6b8e23
147+
OrangeRed=0xff4500
148+
Orchid=0xda70d6
149+
PaleGoldenRod=0xeee8aa
150+
PaleGreen=0x98fb98
151+
PaleTurquoise=0xafeeee
152+
PaleVioletRed=0xdb7093
153+
PapayaWhip=0xffefd5
154+
PeachPuff=0xffdab9
155+
Peru=0xcd853f
156+
Plum=0xdda0dd
157+
PowderBlue=0xb0e0e6
158+
RosyBrown=0xbc8f8f
159+
RoyalBlue=0x4169e1
160+
SaddleBrown=0x8b4513
161+
SandyBrown=0xf4a460
162+
SeaGreen=0x2e8b57
163+
SeaShell=0xfff5ee
164+
Sienna=0xa0522d
165+
SlateBlue=0x6a5acd
166+
SlateGray=0x708090
167+
Snow=0xfffafa
168+
SpringGreen=0x00ff7f
169+
SteelBlue=0x4682b4
170+
Tan=0xd2b48c
171+
Thistle=0xd8bfd8
172+
Tomato=0xff6347
173+
Violet=0xee82ee
174+
Wheat=0xf5deb3
175+
WhiteSmoke=0xf5f5f5
176+
YellowGreen=0x9acd32
39177

40178
def datetime2uno( dt):
41179
r=createUnoStruct("com.sun.star.util.DateTime")

unogenerator/demo.py

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -520,30 +520,59 @@ def demo_ods_sheet_styles(doc):
520520
doc.setCellName("A1", "MYNAME")
521521

522522

523-
headers=[_("Style name"), _("Date and time"), _("Date"), _("Integer"), _("Euros"), _("Dollars"), _("Percentage"), _("Number with 2 decimals"), _("Number with 6 decimals"), _("Time"), _("Boolean")]
523+
headers=[_("Color name"), _("Hex"), _("Date and time"), _("Date"), _("Integer"), _("Euros"), _("Dollars"), _("Percentage"), _("Number with 2 decimals"), _("Number with 6 decimals"), _("Time"), _("Boolean")]
524524
doc.addRowWithStyle( "A1", headers, ColorsNamed.Orange, "BoldCenter")
525525

526-
colors_list=([a for a in dir(ColorsNamed()) if not a.startswith('__')])
527-
for row, color_str in enumerate(colors_list):
528-
color_key=getattr(ColorsNamed(), color_str)
529-
doc.addCellWithStyle(Coord("A2").addRow(row), color_str, color_key, "Bold")
530-
doc.addCellWithStyle(Coord("B2").addRow(row), datetime.now(), color_key, "Datetime")
531-
doc.addCellWithStyle(Coord("C2").addRow(row), date.today(), color_key, "Date")
532-
doc.addCellWithStyle(Coord("D2").addRow(row), pow(-1, row)*-10000000, color_key, "Integer")
533-
doc.addCellWithStyle(Coord("E2").addRow(row), Currency(pow(-1, row)*12.56, "EUR"), color_key, "EUR")
534-
doc.addCellWithStyle(Coord("F2").addRow(row), Currency(pow(-1, row)*12345.56, "USD"), color_key, "USD")
535-
doc.addCellWithStyle(Coord("G2").addRow(row), Percentage(pow(-1, row)*1, 3), color_key, "Percentage")
536-
doc.addCellWithStyle(Coord("H2").addRow(row), pow(-1, row)*123456789.121212, color_key, "Float6")
537-
doc.addCellWithStyle(Coord("I2").addRow(row), pow(-1, row)*-12.121212, color_key, "Float2")
538-
doc.addCellWithStyle(Coord("J2").addRow(row), (datetime.now()+timedelta(seconds=3600*12*row)).time(), color_key, "Time")
539-
doc.addCellWithStyle(Coord("K2").addRow(row), bool(row%2), color_key, "Bool")
540-
541-
doc.addCellWithStyle(Coord("E2").addRow(row+1),f"=sum(E2:{Coord('E2').addRow(row).string()})", ColorsNamed.GrayLight, "EUR" )
542-
doc.addCellMergedWithStyle("E15:K15", "Merge proof", ColorsNamed.Yellow, style="BoldCenter")
543-
doc.setComment("B14", "This is nice comment")
526+
# Get colors and sort them by affinity (Hue)
527+
def rgb_to_hsv(rgb):
528+
r = ((rgb >> 16) & 0xff) / 255.0
529+
g = ((rgb >> 8) & 0xff) / 255.0
530+
b = (rgb & 0xff) / 255.0
531+
mx = max(r, g, b)
532+
mn = min(r, g, b)
533+
df = mx - mn
534+
if mx == mn:
535+
h = 0
536+
elif mx == r:
537+
h = (60 * ((g - b) / df) + 360) % 360
538+
elif mx == g:
539+
h = (60 * ((b - r) / df) + 120) % 360
540+
elif mx == b:
541+
h = (60 * ((r - g) / df) + 240) % 360
542+
if mx == 0:
543+
s = 0
544+
else:
545+
s = df / mx
546+
v = mx
547+
return h, s, v
548+
549+
colors_list = [a for a in dir(ColorsNamed()) if not a.startswith('__')]
550+
# Decorate with hsv for sorting
551+
decorated = []
552+
for color_str in colors_list:
553+
val = getattr(ColorsNamed(), color_str)
554+
decorated.append((rgb_to_hsv(val), color_str, val))
555+
556+
# Sort by Hue, then Saturation, then Value
557+
decorated.sort()
544558

559+
for row, (hsv, color_str, color_key) in enumerate(decorated):
560+
hex_str = f"#{color_key:06X}"
561+
doc.addCellWithStyle(Coord("A2").addRow(row), color_str, color_key, "Bold")
562+
doc.addCellWithStyle(Coord("B2").addRow(row), hex_str, color_key, "Normal")
563+
doc.addCellWithStyle(Coord("C2").addRow(row), datetime.now(), color_key, "Datetime")
564+
doc.addCellWithStyle(Coord("D2").addRow(row), date.today(), color_key, "Date")
565+
doc.addCellWithStyle(Coord("E2").addRow(row), pow(-1, row)*-10000000, color_key, "Integer")
566+
doc.addCellWithStyle(Coord("F2").addRow(row), Currency(pow(-1, row)*12.56, "EUR"), color_key, "EUR")
567+
doc.addCellWithStyle(Coord("G2").addRow(row), Currency(pow(-1, row)*12345.56, "USD"), color_key, "USD")
568+
doc.addCellWithStyle(Coord("H2").addRow(row), Percentage(pow(-1, row)*1, 3), color_key, "Percentage")
569+
doc.addCellWithStyle(Coord("I2").addRow(row), pow(-1, row)*123456789.121212, color_key, "Float6")
570+
doc.addCellWithStyle(Coord("J2").addRow(row), pow(-1, row)*-12.121212, color_key, "Float2")
571+
doc.addCellWithStyle(Coord("K2").addRow(row), (datetime.now()+timedelta(seconds=3600*12*row)).time(), color_key, "Time")
572+
doc.addCellWithStyle(Coord("L2").addRow(row), bool(row%2), color_key, "Bool")
573+
545574
doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS)
546-
doc.freezeAndSelect("B2")
575+
doc.freezeAndSelect("C2")
547576

548577

549578

@@ -731,4 +760,4 @@ def demo_ods_columns_width_modes(doc):
731760

732761
doc.createSheet("Width FROM_LIST")
733762
doc.addListOfRowsWithStyle("A1", [lol_numbers[0]])
734-
doc.setColumnsWidth(lol_numbers[0], types.ColumnsWidthMode.FROM_LIST)
763+
doc.setColumnsWidth(lol_numbers[0], types.ColumnsWidthMode.FROM_LIST)

0 commit comments

Comments
 (0)