numbers-parser 4.8.1__py3-none-any.whl → 4.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
numbers_parser/cell.py CHANGED
@@ -1,9 +1,10 @@
1
1
  import re
2
2
  from collections import namedtuple
3
- from dataclasses import dataclass
3
+ from dataclasses import dataclass, field
4
4
  from datetime import datetime as builtin_datetime
5
5
  from datetime import timedelta as builtin_timedelta
6
6
  from enum import IntEnum
7
+ from os.path import basename
7
8
  from typing import Any, List, Tuple, Union
8
9
  from warnings import warn
9
10
 
@@ -27,6 +28,7 @@ from numbers_parser.constants import (
27
28
  EMPTY_STORAGE_BUFFER,
28
29
  MAX_BASE,
29
30
  MAX_SIGNIFICANT_DIGITS,
31
+ ControlFormattingType,
30
32
  CustomFormattingType,
31
33
  FormattingType,
32
34
  FormatType,
@@ -75,18 +77,43 @@ __all__ = [
75
77
 
76
78
 
77
79
  class BackgroundImage:
78
- def __init__(self, image_data: bytes = None, filename: str = None):
79
- self._data = image_data
80
- self._filename = filename
80
+ """
81
+ A named document style that can be applied to cells.
82
+
83
+ .. code-block:: python
84
+
85
+ fh = open("cats.png", mode="rb")
86
+ image_data = fh.read()
87
+ cats_bg = doc.add_style(
88
+ name="Cats",
89
+ bg_image=BackgroundImage(image_data, "cats.png")
90
+ )
91
+ table.write(0, 0, "❤️ cats", style=cats_bg)
92
+
93
+ Currently only standard image files and not 'advanced' image fills are
94
+ supported. Tiling and scaling is not reported back and cannot be changed
95
+ when saving new cells.
96
+
97
+ Parameters
98
+ ----------
99
+ data: bytes
100
+ Raw image data for a cell background image.
101
+ filename: str
102
+ Path to the image file.
103
+ """
104
+
105
+ def __init__(self, data: bytes = None, filename: str = None):
106
+ self._data = data
107
+ self._filename = basename(filename)
81
108
 
82
109
  @property
83
110
  def data(self) -> bytes:
84
- """The background image as byts for a cell, or None if no image."""
111
+ """bytes: The background image as bytes for a cell, or None if no image."""
85
112
  return self._data
86
113
 
87
114
  @property
88
115
  def filename(self) -> str:
89
- """The image filename for a cell, or None if no image."""
116
+ """str: The image filename for a cell, or None if no image."""
90
117
  return self._filename
91
118
 
92
119
 
@@ -184,6 +211,8 @@ class Style:
184
211
  ------
185
212
  TypeError:
186
213
  If arguments do not match the specified type or for objects have invalid arguments
214
+ IndexError:
215
+ If an image filename already exists in document
187
216
  """
188
217
 
189
218
  alignment: Alignment = DEFAULT_ALIGNMENT_CLASS # : horizontal and vertical alignment
@@ -230,6 +259,7 @@ class Style:
230
259
  return [
231
260
  "alignment",
232
261
  "bg_color",
262
+ "bg_image",
233
263
  "first_indent",
234
264
  "left_indent",
235
265
  "right_indent",
@@ -273,9 +303,9 @@ class Style:
273
303
  if not isinstance(self.font_name, str):
274
304
  raise TypeError("font name must be a string")
275
305
 
276
- for field in ["bold", "italic", "underline", "strikethrough"]:
277
- if not isinstance(getattr(self, field), bool):
278
- raise TypeError(f"{field} argument must be boolean")
306
+ for attr in ["bold", "italic", "underline", "strikethrough"]:
307
+ if not isinstance(getattr(self, attr), bool):
308
+ raise TypeError(f"{attr} argument must be boolean")
279
309
 
280
310
  def __setattr__(self, name: str, value: Any) -> None:
281
311
  """Detect changes to cell styles and flag the style for
@@ -575,8 +605,14 @@ class Cell(Cacheable):
575
605
  else:
576
606
  raise ValueError("Can't determine cell type from type " + type(value).__name__)
577
607
 
578
- def _set_formatting(self, format_id: int, format_type: FormattingType) -> None:
579
- self._storage._set_formatting(format_id, format_type)
608
+ def _set_formatting(
609
+ self,
610
+ format_id: int,
611
+ format_type: FormattingType,
612
+ control_id: int = None,
613
+ is_currency: bool = False,
614
+ ) -> None:
615
+ self._storage.set_formatting(format_id, format_type, control_id, is_currency)
580
616
 
581
617
  def __init__(self, row: int, col: int, value):
582
618
  self._value = value
@@ -705,7 +741,32 @@ class Cell(Cacheable):
705
741
 
706
742
  @property
707
743
  def formatted_value(self) -> str:
708
- """str: The formatted value of the cell as it appears in Numbers."""
744
+ """
745
+ str: The formatted value of the cell as it appears in Numbers.
746
+
747
+ Interactive elements are converted into a suitable text format where
748
+ supported, or as their number values where there is no suitable
749
+ visual representation. Currently supported mappings are:
750
+
751
+ * Checkboxes are U+2610 (Ballow Box) or U+2611 (Ballot Box with Check)
752
+ * Ratings are their star value represented using (U+2605) (Black Star)
753
+
754
+ .. code-block:: python
755
+
756
+ >>> table = doc.sheets[0].tables[0]
757
+ >>> table.cell(0,0).value
758
+ False
759
+ >>> table.cell(0,0).formatted_value
760
+ '☐'
761
+ >>> table.cell(0,1).value
762
+ True
763
+ >>> table.cell(0,1).formatted_value
764
+ '☑'
765
+ >>> table.cell(1,1).value
766
+ 3.0
767
+ >>> table.cell(1,1).formatted_value
768
+ '★★★'
769
+ """
709
770
  if self._storage is None:
710
771
  return ""
711
772
  else:
@@ -1079,16 +1140,22 @@ def xl_col_to_name(col, col_abs=False):
1079
1140
 
1080
1141
  @dataclass()
1081
1142
  class Formatting:
1082
- type: FormattingType = FormattingType.NUMBER
1143
+ allow_none: bool = False
1083
1144
  base_places: int = 0
1084
1145
  base_use_minus_sign: bool = True
1085
1146
  base: int = 10
1147
+ control_format: ControlFormattingType = ControlFormattingType.NUMBER
1086
1148
  currency_code: str = "GBP"
1087
1149
  date_time_format: str = DEFAULT_DATETIME_FORMAT
1088
1150
  decimal_places: int = None
1089
1151
  fraction_accuracy: FractionAccuracy = FractionAccuracy.THREE
1152
+ increment: float = 1.0
1153
+ maximum: float = 100.0
1154
+ minimum: float = 1.0
1155
+ popup_values: List[str] = field(default_factory=lambda: ["Item 1"])
1090
1156
  negative_style: NegativeNumberStyle = NegativeNumberStyle.MINUS
1091
1157
  show_thousands_separator: bool = False
1158
+ type: FormattingType = FormattingType.NUMBER
1092
1159
  use_accounting_style: bool = False
1093
1160
  _format_id = None
1094
1161
 
@@ -11,6 +11,8 @@ from pendulum import datetime, duration
11
11
 
12
12
  from numbers_parser import __name__ as numbers_parser_name
13
13
  from numbers_parser.constants import (
14
+ CHECKBOX_FALSE_VALUE,
15
+ CHECKBOX_TRUE_VALUE,
14
16
  CURRENCY_CELL_TYPE,
15
17
  CUSTOM_TEXT_PLACEHOLDER,
16
18
  DATETIME_FIELD_MAP,
@@ -21,6 +23,7 @@ from numbers_parser.constants import (
21
23
  SECONDS_IN_DAY,
22
24
  SECONDS_IN_HOUR,
23
25
  SECONDS_IN_WEEK,
26
+ STAR_RATING_VALUE,
24
27
  CellPadding,
25
28
  CellType,
26
29
  CustomFormattingType,
@@ -60,7 +63,7 @@ class CellStorage(Cacheable):
60
63
  # "cond_style_id",
61
64
  # "cond_rule_style_id",
62
65
  "formula_id",
63
- # "control_id",
66
+ "control_id",
64
67
  "formula_error_id",
65
68
  "suggest_id",
66
69
  "num_format_id",
@@ -95,7 +98,7 @@ class CellStorage(Cacheable):
95
98
  # self.cond_style_id = None
96
99
  # self.cond_rule_style_id = None
97
100
  self.formula_id = None
98
- # self.control_id = None
101
+ self.control_id = None
99
102
  self.formula_error_id = None
100
103
  self.suggest_id = None
101
104
  self.num_format_id = None
@@ -148,9 +151,9 @@ class CellStorage(Cacheable):
148
151
  if flags & 0x200:
149
152
  self.formula_id = unpack("<i", buffer[offset : offset + 4])[0]
150
153
  offset += 4
151
- # if flags & 0x400:
152
- # self.control_id = unpack("<i", buffer[offset : offset + 4])[0]
153
- # offset += 4
154
+ if flags & 0x400:
155
+ self.control_id = unpack("<i", buffer[offset : offset + 4])[0]
156
+ offset += 4
154
157
  # if flags & 0x800:
155
158
  # self.formula_error_id = unpack("<i", buffer[offset : offset + 4])[0]
156
159
  # offset += 4
@@ -158,7 +161,7 @@ class CellStorage(Cacheable):
158
161
  self.suggest_id = unpack("<i", buffer[offset : offset + 4])[0]
159
162
  offset += 4
160
163
  # Skip unused flags
161
- offset += 4 * bin(flags & 0xD00).count("1")
164
+ offset += 4 * bin(flags & 0x900).count("1")
162
165
  #
163
166
  if flags & 0x2000:
164
167
  self.num_format_id = unpack("<i", buffer[offset : offset + 4])[0]
@@ -300,13 +303,15 @@ class CellStorage(Cacheable):
300
303
  format = self.model.table_format(self.table_id, self.text_format_id)
301
304
  elif self.currency_format_id is not None:
302
305
  format = self.model.table_format(self.table_id, self.currency_format_id)
306
+ elif self.bool_format_id is not None and self.type == CellType.BOOL:
307
+ format = self.model.table_format(self.table_id, self.bool_format_id)
303
308
  elif self.num_format_id is not None:
304
309
  format = self.model.table_format(self.table_id, self.num_format_id)
305
- elif self.bool_format_id is not None:
306
- format = self.model.table_format(self.table_id, self.bool_format_id)
307
310
  else:
308
311
  return str(self.value)
309
312
 
313
+ debug("custom_format: @[%d,%d]: format_type=%s, ", self.row, self.col, format.format_type)
314
+
310
315
  if format.HasField("custom_uid"):
311
316
  format_uuid = NumbersUUID(format.custom_uid).hex
312
317
  format_map = self.model.custom_format_map()
@@ -336,6 +341,10 @@ class CellStorage(Cacheable):
336
341
  return format_fraction(self.d128, format)
337
342
  elif format.format_type == FormatType.SCIENTIFIC:
338
343
  return format_scientific(self.d128, format)
344
+ elif format.format_type == FormatType.CHECKBOX:
345
+ return CHECKBOX_TRUE_VALUE if self.value else CHECKBOX_FALSE_VALUE
346
+ elif format.format_type == FormatType.RATING:
347
+ return STAR_RATING_VALUE * int(self.d128)
339
348
  else:
340
349
  formatted_value = str(self.value)
341
350
  return formatted_value
@@ -362,6 +371,14 @@ class CellStorage(Cacheable):
362
371
 
363
372
  def duration_format(self) -> str:
364
373
  format = self.model.table_format(self.table_id, self.duration_format_id)
374
+ debug(
375
+ "duration_format: @[%d,%d]: table_id=%d, duration_format_id=%d, duration_style=%s",
376
+ self.row,
377
+ self.col,
378
+ self.table_id,
379
+ self.duration_format_id,
380
+ format.duration_style,
381
+ )
365
382
 
366
383
  duration_style = format.duration_style
367
384
  unit_largest = format.duration_unit_largest
@@ -431,15 +448,34 @@ class CellStorage(Cacheable):
431
448
 
432
449
  return duration_str
433
450
 
434
- def _set_formatting(
435
- self, format_id: int, format_type: Union[FormattingType, CustomFormattingType]
451
+ def set_formatting(
452
+ self,
453
+ format_id: int,
454
+ format_type: Union[FormattingType, CustomFormattingType],
455
+ control_id: int = None,
456
+ is_currency: bool = False,
436
457
  ) -> None:
458
+ self.is_currency = is_currency
437
459
  if format_type == FormattingType.CURRENCY:
438
460
  self.currency_format_id = format_id
439
- self.is_currency = True
461
+ elif format_type == FormattingType.TICKBOX:
462
+ self.bool_format_id = format_id
463
+ self.control_id = control_id
464
+ elif format_type == FormattingType.RATING:
465
+ self.num_format_id = format_id
466
+ self.control_id = control_id
467
+ elif format_type in [FormattingType.SLIDER, FormattingType.STEPPER]:
468
+ if is_currency:
469
+ self.currency_format_id = format_id
470
+ else:
471
+ self.num_format_id = format_id
472
+ self.control_id = control_id
473
+ elif format_type == FormattingType.POPUP:
474
+ self.text_format_id = format_id
475
+ self.control_id = control_id
440
476
  elif format_type in [FormattingType.DATETIME, CustomFormattingType.DATETIME]:
441
477
  self.date_format_id = format_id
442
- elif format_type == CustomFormattingType.TEXT:
478
+ elif format_type in [FormattingType.TEXT, CustomFormattingType.TEXT]:
443
479
  self.text_format_id = format_id
444
480
  else:
445
481
  self.num_format_id = format_id
@@ -20,6 +20,7 @@ __all__ = [
20
20
  "FormattingType",
21
21
  "NegativeNumberStyle",
22
22
  "FractionAccuracy",
23
+ "ControlFormattingType",
23
24
  ]
24
25
 
25
26
  DEFAULT_DOCUMENT = files("numbers_parser") / "data" / "empty.numbers"
@@ -44,8 +45,11 @@ DEFAULT_TEXT_INSET = 4.0
44
45
  DEFAULT_TEXT_WRAP = True
45
46
  EMPTY_STORAGE_BUFFER = b"\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
46
47
 
47
- # Formatting defaults
48
+ # Formatting values and defaults
48
49
  DEFAULT_DATETIME_FORMAT = "dd MMM YYY HH:MM"
50
+ CHECKBOX_FALSE_VALUE = "☐"
51
+ CHECKBOX_TRUE_VALUE = "☑"
52
+ STAR_RATING_VALUE = "★"
49
53
 
50
54
  # Numbers limits
51
55
  MAX_TILE_SIZE = 256
@@ -179,6 +183,39 @@ class FormattingType(IntEnum):
179
183
  NUMBER = 5
180
184
  PERCENTAGE = 6
181
185
  SCIENTIFIC = 7
186
+ TICKBOX = 8
187
+ RATING = 9
188
+ SLIDER = 10
189
+ STEPPER = 11
190
+ POPUP = 12
191
+ TEXT = 13
192
+
193
+
194
+ class ControlFormattingType(IntEnum):
195
+ BASE = 1
196
+ CURRENCY = 2
197
+ FRACTION = 4
198
+ NUMBER = 5
199
+ PERCENTAGE = 6
200
+ SCIENTIFIC = 7
201
+
202
+
203
+ FORMATTING_ALLOWED_CELLS = {
204
+ "base": ["NumberCell"],
205
+ "currency": ["NumberCell"],
206
+ "datetime": ["DateCell"],
207
+ "fraction": ["NumberCell"],
208
+ "number": ["NumberCell"],
209
+ "percentage": ["NumberCell"],
210
+ "popup": ["NumberCell", "TextCell"],
211
+ "rating": ["NumberCell"],
212
+ "scientific": ["NumberCell"],
213
+ "slider": ["NumberCell"],
214
+ "stepper": ["NumberCell"],
215
+ "tickbox": ["BoolCell"],
216
+ }
217
+
218
+ FORMATTING_ACTION_CELLS = ["tickbox", "rating", "popup", "slider", "stepper"]
182
219
 
183
220
 
184
221
  class CustomFormattingType(IntEnum):
@@ -187,6 +224,13 @@ class CustomFormattingType(IntEnum):
187
224
  TEXT = 103
188
225
 
189
226
 
227
+ CUSTOM_FORMATTING_ALLOWED_CELLS = {
228
+ "number": ["NumberCell"],
229
+ "datetime": ["DateCell"],
230
+ "text": ["TextCell"],
231
+ }
232
+
233
+
190
234
  @enum_tools.documentation.document_enum
191
235
  class NegativeNumberStyle(IntEnum):
192
236
  """
@@ -236,19 +280,47 @@ class FractionAccuracy(IntEnum):
236
280
 
237
281
 
238
282
  ALLOWED_FORMATTING_PARAMETERS = {
239
- FormattingType.BASE: ["base", "base_places", "base_use_minus_sign"],
283
+ FormattingType.BASE: [
284
+ "base",
285
+ "base_places",
286
+ "base_use_minus_sign",
287
+ ],
240
288
  FormattingType.CURRENCY: [
289
+ "currency_code",
241
290
  "decimal_places",
242
- "show_thousands_separator",
243
291
  "negative_style",
292
+ "show_thousands_separator",
244
293
  "use_accounting_style",
245
- "currency_code",
246
294
  ],
247
295
  FormattingType.DATETIME: ["date_time_format"],
248
296
  FormattingType.FRACTION: ["fraction_accuracy"],
249
- FormattingType.NUMBER: ["decimal_places", "show_thousands_separator", "negative_style"],
250
- FormattingType.PERCENTAGE: ["decimal_places", "show_thousands_separator", "negative_style"],
297
+ FormattingType.NUMBER: [
298
+ "decimal_places",
299
+ "show_thousands_separator",
300
+ "negative_style",
301
+ ],
302
+ FormattingType.PERCENTAGE: [
303
+ "decimal_places",
304
+ "show_thousands_separator",
305
+ "negative_style",
306
+ ],
251
307
  FormattingType.SCIENTIFIC: ["decimal_places"],
308
+ FormattingType.POPUP: ["popup_values", "allow_none"],
309
+ FormattingType.RATING: [],
310
+ FormattingType.SLIDER: [
311
+ "control_format",
312
+ "increment",
313
+ "maximum",
314
+ "minimum",
315
+ ],
316
+ FormattingType.STEPPER: [
317
+ "control_format",
318
+ "increment",
319
+ "maximum",
320
+ "minimum",
321
+ ],
322
+ FormattingType.TICKBOX: [],
323
+ FormattingType.TEXT: [],
252
324
  }
253
325
 
254
326
  FORMAT_TYPE_MAP = {
@@ -258,7 +330,13 @@ FORMAT_TYPE_MAP = {
258
330
  FormattingType.FRACTION: FormatType.FRACTION,
259
331
  FormattingType.NUMBER: FormatType.DECIMAL,
260
332
  FormattingType.PERCENTAGE: FormatType.PERCENT,
333
+ FormattingType.POPUP: FormatType.TEXT,
334
+ FormattingType.RATING: FormatType.RATING,
261
335
  FormattingType.SCIENTIFIC: FormatType.SCIENTIFIC,
336
+ FormattingType.SLIDER: FormatType.DECIMAL,
337
+ FormattingType.STEPPER: FormatType.DECIMAL,
338
+ FormattingType.TICKBOX: FormatType.CHECKBOX,
339
+ FormattingType.TEXT: FormatType.TEXT,
262
340
  }
263
341
 
264
342
  CUSTOM_FORMAT_TYPE_MAP = {
@@ -268,6 +346,25 @@ CUSTOM_FORMAT_TYPE_MAP = {
268
346
  }
269
347
 
270
348
 
349
+ class CellInteractionType(IntEnum):
350
+ VALUE_EDITING = 0
351
+ FORMULA_EDITING = 1
352
+ STOCK = 2
353
+ CATEGORY_SUMMARY = 3
354
+ STEPPER = 4
355
+ SLIDER = 5
356
+ RATING = 6
357
+ POPUP = 7
358
+ TOGGLE = 8
359
+
360
+
361
+ CONTROL_CELL_TYPE_MAP = {
362
+ FormattingType.POPUP: CellInteractionType.POPUP,
363
+ FormattingType.SLIDER: CellInteractionType.SLIDER,
364
+ FormattingType.STEPPER: CellInteractionType.STEPPER,
365
+ }
366
+
367
+
271
368
  @enum_tools.documentation.document_enum
272
369
  class PaddingType(IntEnum):
273
370
  """How integers and decimals are padded in custom number formats"""
@@ -59,7 +59,7 @@ class ObjectStore:
59
59
  self._objects[PACKAGE_ID].last_object_identifier = self._max_id
60
60
  return self._max_id
61
61
 
62
- def create_object_from_dict(self, iwa_file: str, object_dict: dict, cls: object):
62
+ def create_object_from_dict(self, iwa_file: str, object_dict: dict, cls: object, append=False):
63
63
  """Create a new object and store the associated IWA segment. Return the
64
64
  message ID for the object and the newly created object. If the IWA
65
65
  file cannot be found, it will be created.
@@ -70,7 +70,7 @@ class ObjectStore:
70
70
  new_id = self.new_message_id()
71
71
  iwa_segment = create_iwa_segment(new_id, cls, object_dict)
72
72
 
73
- if iwa_pathname is None:
73
+ if iwa_pathname is None and not append:
74
74
  iwa_pathname = iwa_file.format(new_id) + ".iwa"
75
75
  chunks = {"chunks": [{"archives": [iwa_segment.to_dict()]}]}
76
76
  self._file_store[iwa_pathname] = IWAFile.from_dict(chunks)
@@ -4,15 +4,15 @@ from warnings import warn
4
4
  from pendulum import DateTime, Duration
5
5
 
6
6
  from numbers_parser.cell import (
7
+ BackgroundImage,
7
8
  Border,
8
9
  Cell,
10
+ ControlFormattingType,
9
11
  CustomFormatting,
10
12
  CustomFormattingType,
11
- DateCell,
12
13
  Formatting,
13
14
  FormattingType,
14
15
  MergedCell,
15
- NumberCell,
16
16
  Style,
17
17
  TextCell,
18
18
  UnsupportedWarning,
@@ -21,8 +21,11 @@ from numbers_parser.cell import (
21
21
  )
22
22
  from numbers_parser.cell_storage import CellStorage
23
23
  from numbers_parser.constants import (
24
+ CUSTOM_FORMATTING_ALLOWED_CELLS,
24
25
  DEFAULT_COLUMN_COUNT,
25
26
  DEFAULT_ROW_COUNT,
27
+ FORMATTING_ACTION_CELLS,
28
+ FORMATTING_ALLOWED_CELLS,
26
29
  MAX_COL_COUNT,
27
30
  MAX_HEADER_COUNT,
28
31
  MAX_ROW_COUNT,
@@ -221,10 +224,17 @@ class Document:
221
224
  ------
222
225
  TypeError:
223
226
  If ``font_size`` is not a ``float``, ``font_name`` is not a ``str``,
227
+ ``bg_image`` is not a :py:class:`~numbers_parser.BackgroundImage`,
224
228
  or if any of the ``bool`` parameters are invalid.
225
229
  """ # noqa: E501
226
230
  if "name" in kwargs and kwargs["name"] is not None and kwargs["name"] in self._model.styles:
227
231
  raise IndexError(f"style '{kwargs['name']}' already exists")
232
+
233
+ if "bg_image" in kwargs and kwargs["bg_image"] is not None:
234
+ if not isinstance(kwargs["bg_image"], BackgroundImage):
235
+ raise TypeError("bg_image must be a BackgroundImage object")
236
+ self._model.store_image((kwargs["bg_image"].data), kwargs["bg_image"].filename)
237
+
228
238
  style = Style(**kwargs)
229
239
  if style.name is None:
230
240
  style.name = self._model.custom_style_name()
@@ -814,20 +824,28 @@ class Table(Cacheable): # noqa: F811
814
824
  The ``write()`` method supports two forms of notation to designate the position
815
825
  of cells: **Row-column** notation and **A1** notation:
816
826
 
817
- .. code-block:: python
827
+ .. code:: python
818
828
 
819
- (0, 0) # Row-column notation.
820
- ("A1") # The same cell in A1 notation.
829
+ doc = Document("write.numbers")
830
+ sheets = doc.sheets
831
+ tables = sheets[0].tables
832
+ table = tables[0]
833
+ table.write(1, 1, "This is new text")
834
+ table.write("B7", datetime(2020, 12, 25))
835
+ doc.save("new-sheet.numbers")
821
836
 
822
837
  Parameters
823
838
  ----------
824
839
 
825
- param1: int
840
+ row: int
826
841
  The row number (zero indexed)
827
- param2: int
842
+ col: int
828
843
  The column number (zero indexed)
829
- param3: str | int | float | bool | DateTime | Duration
830
- The value to write to the cell. The generated cell type
844
+ value: str | int | float | bool | DateTime | Duration
845
+ The value to write to the cell. The generated cell type is automatically
846
+ created based on the type of ``value``.
847
+ style: Style | str | None
848
+ The name of a document custom style or a :py:class:`~numbers_parser.cell.Style` object.
831
849
 
832
850
  Warns
833
851
  -----
@@ -843,19 +861,6 @@ class Table(Cacheable): # noqa: F811
843
861
  If the style parameter is an invalid type.
844
862
  ValueError:
845
863
  If the cell type cannot be determined from the type of `param3`.
846
-
847
- Example
848
- -------
849
-
850
- .. code:: python
851
-
852
- doc = Document("write.numbers")
853
- sheets = doc.sheets
854
- tables = sheets[0].tables
855
- table = tables[0]
856
- table.write(1, 1, "This is new text")
857
- table.write("B7", datetime(2020, 12, 25))
858
- doc.save("new-sheet.numbers")
859
864
  """
860
865
  # TODO: write needs to retain/init the border
861
866
  (row, col, value) = self._validate_cell_coords(*args)
@@ -1103,6 +1108,48 @@ class Table(Cacheable): # noqa: F811
1103
1108
  cell._set_merge(merge_cells.get((row, col)))
1104
1109
 
1105
1110
  def set_cell_border(self, *args):
1111
+ """
1112
+ Set the borders for a cell.
1113
+
1114
+ Cell references can be row-column offsers or Excel/Numbers-style A1 notation. Borders
1115
+ can be applied to multiple sides of a cell by passing a list of sides. The name(s)
1116
+ of the side(s) must be one of ``"top"``, ``"right"``, ``"bottom"`` or ``"left"``.
1117
+
1118
+ Numbers supports different border styles for each cell within a merged cell range
1119
+ for those cells that are on the outer part of the merge. ``numbers-parser`` will
1120
+ ignore attempts to set these invisible cell edges and issue a ``RuntimeWarning``.
1121
+
1122
+ .. code-block:: python
1123
+
1124
+ # Dashed line for B7's right border
1125
+ table.set_cell_border(6, 1, "right", Border(5.0, RGB(29, 177, 0), "dashes"))
1126
+ # Solid line starting at B7's left border and running for 3 rows
1127
+ table.set_cell_border("B7", "left", Border(8.0, RGB(29, 177, 0), "solid"), 3)
1128
+
1129
+ :Args (row-column):
1130
+ * **param1** (*int*): The row number (zero indexed).
1131
+ * **param2** (*int*): The column number (zero indexed).
1132
+ * **param3** (*str | List[str]*): Which side(s) of the cell to apply the border to.
1133
+ * **param4** (:py:class:`Border`): The border to add.
1134
+ * **param5** (*int*, *optinal*, default: 1): The length of the stroke to add.
1135
+
1136
+ :Args (A1):
1137
+ * **param1** (*str*): A cell reference using Excel/Numbers-style A1 notation.
1138
+ * **param2** (*str | List[str]*): Which side(s) of the cell to apply the border to.
1139
+ * **param3** (:py:class:`Border`): The border to add.
1140
+ * **param4** (*int*, *optional*, default: 1): The length of the stroke to add.
1141
+
1142
+ Raises
1143
+ ------
1144
+ TypeError:
1145
+ If an invalid number of arguments is passed or if the types of the arguments
1146
+ are invalid.
1147
+
1148
+ Warns
1149
+ -----
1150
+ RuntimeWarning:
1151
+ If any of the sides to which the border is applied have been merged.
1152
+ """ # noqa: E501
1106
1153
  (row, col, *args) = self._validate_cell_coords(*args)
1107
1154
  if len(args) == 2:
1108
1155
  (side, border_value) = args
@@ -1165,32 +1212,52 @@ class Table(Cacheable): # noqa: F811
1165
1212
 
1166
1213
  Cell references can be **row-column** offsers or Excel/Numbers-style **A1** notation.
1167
1214
 
1215
+ .. code:: python
1216
+
1217
+ table.set_cell_formatting(
1218
+ "C1",
1219
+ "date",
1220
+ date_time_format="EEEE, d MMMM yyyy"
1221
+ )
1222
+ table.set_cell_formatting(
1223
+ 0,
1224
+ 4,
1225
+ "number",
1226
+ decimal_places=3,
1227
+ negative_style=NegativeNumberStyle.RED
1228
+ )
1229
+
1168
1230
  :Parameters:
1169
1231
  * **args** (*list*, *optional*) – Positional arguments for cell reference and data format type (see below)
1170
1232
  * **kwargs** (*dict*, *optional*) - Key-value pairs defining a formatting options for each data format (see below).
1171
1233
 
1172
1234
  :Args (row-column):
1173
- * **param1** (``int``): The row number (zero indexed).
1174
- * **param2** (``int``): The column number (zero indexed).
1175
- * **param3** (``str``): Data format type for the cell (see "data formats" below).
1235
+ * **param1** (*int*): The row number (zero indexed).
1236
+ * **param2** (*int*): The column number (zero indexed).
1237
+ * **param3** (*str*): Data format type for the cell (see "data formats" below).
1176
1238
 
1177
1239
  :Args (A1):
1178
- * **param1** (``str``): A cell reference using Excel/Numbers-style A1 notation.
1179
- * **param2** (``str``): Data format type for the cell (see "data formats" below).
1240
+ * **param1** (*str*): A cell reference using Excel/Numbers-style A1 notation.
1241
+ * **param2** (*str*): Data format type for the cell (see "data formats" below).
1242
+
1243
+ :Raises:
1244
+ * **TypeError** -
1245
+ If a tickbox is chosen for anything other than ``bool`` values.
1180
1246
 
1181
1247
  :Warns:
1182
1248
  * **RuntimeWarning** -
1183
1249
  If ``use_accounting_style`` is used with
1184
- any ``negative_style`` other than ``NegativeNumberStyle.MINUS``.
1250
+ any ``negative_style`` other than ``NegativeNumberStyle.MINUS``, or
1251
+ if a rating is out of range 0 to 5 (rating is clamped to these values).
1185
1252
 
1186
1253
  All formatting styles share a name and a type, described in the **Common**
1187
1254
  parameters in the following table. Additional key-value pairs configure the format
1188
1255
  depending upon the value of ``kwargs["type"]``.
1189
1256
 
1190
1257
  :Common Args:
1191
- * **name** (``str``) – The name of the custom format. If no name is provided,
1258
+ * **name** (*str*) – The name of the custom format. If no name is provided,
1192
1259
  one is generated using the scheme ``Custom Format``, ``Custom Format 1``, ``Custom Format 2``, etc.
1193
- * **type** (``str``, *optional*, default: ``number``) – The type of format to
1260
+ * **type** (*str, optional, default: number*) – The type of format to
1194
1261
  create:
1195
1262
 
1196
1263
  * ``"base"``: A number base in the range 2-36.
@@ -1200,56 +1267,89 @@ class Table(Cacheable): # noqa: F811
1200
1267
  * ``"percentage"``: A number formatted as a percentage
1201
1268
  * ``"number"``: A decimal number.
1202
1269
  * ``"scientific"``: A decimal number with scientific notation.
1270
+ * ``"tickbox"``: A checkbox (bool values only).
1271
+ * ``"rating"``: A star rating from 0 to 5.
1272
+ * ``"slider"``: A range slider.
1273
+ * ``"stepper"``: An up/down value stepper.
1274
+ * ``"popup"``: A menu of options.
1203
1275
 
1204
1276
  :``"base"``:
1205
- * **base_use_minus_sign** (``int``, *optional*, default: ``10``) – The integer
1277
+ * **base_use_minus_sign** (*int, optional, default: 10*) – The integer
1206
1278
  base to represent the number from 2-36.
1207
- * **base_use_minus_sign** (``bool``, *optional*, default: ``True``) – If ``True``
1279
+ * **base_use_minus_sign** (*bool, optional, default: True*) – If ``True``
1208
1280
  use a standard minus sign, otherwise format as two's compliment (only
1209
1281
  possible for binary, octal and hexadecimal.
1210
- * **base_places** (``int``, *optional*, default: ``0``) – The number of
1282
+ * **base_places** (*int, optional, default: 0*) – The number of
1211
1283
  decimal places, or ``None`` for automatic.
1212
1284
 
1213
1285
  :``"currency"``:
1214
- * **currency** (``str``, *optional*, default: ``"GBP"``) – An ISO currency
1286
+ * **currency** (*str, optional, default: "GBP"*) – An ISO currency
1215
1287
  code, e.g. ``"GBP"`` or ``"USD"``.
1216
- * **decimal_places** (``int``, *optional*, default: ``2``) – The number of
1288
+ * **decimal_places** (*int, optional, default: 2*) – The number of
1217
1289
  decimal places, or ``None`` for automatic.
1218
- * **negative_style** (:py:class:`~numbers_parser.NegativeNumberStyle`, *optional*, default: ``NegativeNumberStyle.MINUS``) – How negative numbers are represented.
1290
+ * **negative_style** (*:py:class:`~numbers_parser.NegativeNumberStyle`, optional, default: NegativeNumberStyle.MINUS*) – How negative numbers are represented.
1219
1291
  See `Negative number formats <#negative-formats>`_.
1220
- * **show_thousands_separator** (``bool``, *optional*, default: ``False``) – ``True``
1292
+ * **show_thousands_separator** (*bool, optional, default: False*) – ``True``
1221
1293
  if the number should include a thousands seperator, e.g. ``,``
1222
- * **use_accounting_style** (``bool``, *optional*, default: ``False``) – ``True``
1294
+ * **use_accounting_style** (*bool, optional, default: False*) – ``True``
1223
1295
  if the currency symbol should be formatted to the left of the cell and
1224
1296
  separated from the number value by a tab.
1225
1297
 
1226
1298
  :``"datetime"``:
1227
- * **date_time_format** (``str``, *optional*, default: ``"dd MMM YYY HH:MM"``) – A POSIX
1299
+ * **date_time_format** (*str, optional, default: "dd MMM YYY HH:MM"*) – A POSIX
1228
1300
  strftime-like formatting string of `Numbers date/time
1229
1301
  directives <#datetime-formats>`_.
1230
1302
 
1231
1303
  :``"fraction"``:
1232
- * **fraction_accuracy** (:py:class:`~numbers_parser.FractionAccuracy`, *optional*, default: ``FractionAccuracy.THREE`` – The
1304
+ * **fraction_accuracy** (*:py:class:`~numbers_parser.FractionAccuracy`, optional, default: FractionAccuracy.THREE* – The
1233
1305
  precision of the faction.
1234
1306
 
1235
1307
  :``"percentage"``:
1236
- * **decimal_places** (``float``, *optional*, default: ``None``) – number of
1308
+ * **decimal_places** (*float, optional, default: None*) – number of
1237
1309
  decimal places, or ``None`` for automatic.
1238
- * **negative_style** (:py:class:`~numbers_parser.NegativeNumberStyle`, *optional*, default: ``NegativeNumberStyle.MINUS``) – How negative numbers are represented.
1310
+ * **negative_style** (*:py:class:`~numbers_parser.NegativeNumberStyle`, optional, default: NegativeNumberStyle.MINUS*) – How negative numbers are represented.
1239
1311
  See `Negative number formats <#negative-formats>`_.
1240
- * **show_thousands_separator** (``bool``, *optional*, default: ``False``) – ``True``
1312
+ * **show_thousands_separator** (*bool, optional, default: False*) – ``True``
1241
1313
  if the number should include a thousands seperator, e.g. ``,``
1242
1314
 
1243
1315
  :``"scientific"``:
1244
- * **decimal_places** (``float``, *optional*, default: ``None``) – number of
1316
+ * **decimal_places** (*float, optional, default: None*) – number of
1245
1317
  decimal places, or ``None`` for automatic.
1246
1318
 
1247
- Example
1319
+ :``"tickbox"``:
1320
+ * No additional parameters defined.
1321
+
1322
+ :``"rating"``:
1323
+ * No additional parameters defined.
1324
+
1325
+ :``"slider"``:
1326
+ * **control_format** (*ControlFormattingType, optional, default: ControlFormattingType.NUMBER*) - the format
1327
+ of the data in the slider. Valid options are ``"base"``, ``"currency"``,
1328
+ ``"datetime"``, ``"fraction"``, ``"percentage"``, ``"number"``,
1329
+ or ``"scientific". Each format allows additional parameters identical to those
1330
+ available for the formats themselves. For example, a slider using fractions
1331
+ is configured with ``fraction_accuracy``.
1332
+ * **increment** (*float, optional, default: 1*) - the slider's minimum value
1333
+ * **maximum** (*float, optional, default: 100*) - the slider's maximum value
1334
+ * **minimum** (*float, optional, default: 1*) - increment value for the slider
1335
+
1336
+ :`"stepper"``:
1337
+ * **control_format** (*ControlFormattingType, optional, default: ControlFormattingType.NUMBER*) - the format
1338
+ of the data in the stepper. Valid options are ``"base"``, ``"currency"``,
1339
+ ``"datetime"``, ``"fraction"``, ``"percentage"``, ``"number"``,
1340
+ or ``"scientific"``. Each format allows additional parameters identical to those
1341
+ available for the formats themselves. For example, a stepper using fractions
1342
+ is configured with ``fraction_accuracy``.
1343
+ * **increment** (*float, optional, default: 1*) - the stepper's minimum value
1344
+ * **maximum** (*float, optional, default: 100*) - the stepper's maximum value
1345
+ * **minimum** (*float, optional, default: 1*) - increment value for the stepper
1346
+
1347
+ :`"popup"``:
1348
+ * **popup_values** (*List[str|int|float], optional, default: None*) – values
1349
+ for the popup menu
1350
+ * **allow_none** (*bool, optional, default: True*) - If ``True``
1351
+ include a blank value in the list
1248
1352
 
1249
- .. code:: python
1250
-
1251
- >>> table.set_cell_formatting("C1", "date", date_time_format="EEEE, d MMMM yyyy")
1252
- >>> table.set_cell_formatting(0, 4, "number", decimal_places=3, negative_style=NegativeNumberStyle.RED)
1253
1353
 
1254
1354
  """ # noqa: E501
1255
1355
  (row, col, *args) = self._validate_cell_coords(*args)
@@ -1280,33 +1380,57 @@ class Table(Cacheable): # noqa: F811
1280
1380
  raise TypeError("format must be a CustomFormatting object or format name")
1281
1381
 
1282
1382
  cell = self._data[row][col]
1283
- if custom_format.type == CustomFormattingType.DATETIME and not isinstance(cell, DateCell):
1284
- type_name = type(cell).__name__
1285
- raise TypeError(f"cannot use date/time formatting for cells of type {type_name}")
1286
- elif custom_format.type == CustomFormattingType.NUMBER and not isinstance(cell, NumberCell):
1287
- type_name = type(cell).__name__
1288
- raise TypeError(f"cannot use number formatting for cells of type {type_name}")
1289
- elif custom_format.type == CustomFormattingType.TEXT and not isinstance(cell, TextCell):
1290
- type_name = type(cell).__name__
1291
- raise TypeError(f"cannot use text formatting for cells of type {type_name}")
1383
+ type_name = type(cell).__name__
1384
+ format_type_name = custom_format.type.name.lower()
1385
+ if type_name not in CUSTOM_FORMATTING_ALLOWED_CELLS[format_type_name]:
1386
+ raise TypeError(
1387
+ f"cannot use {format_type_name} formatting for cells of type {type_name}"
1388
+ )
1292
1389
 
1293
1390
  format_id = self._model.custom_format_id(self._table_id, custom_format)
1294
1391
  cell._set_formatting(format_id, custom_format.type)
1295
1392
 
1296
- def _set_cell_data_format(self, row: int, col: int, format_type: str, **kwargs) -> None:
1393
+ def _set_cell_data_format(self, row: int, col: int, format_type_name: str, **kwargs) -> None:
1297
1394
  try:
1298
- format_type = FormattingType[format_type.upper()]
1395
+ format_type = FormattingType[format_type_name.upper()]
1299
1396
  except (KeyError, AttributeError):
1300
- raise TypeError(f"unsuported cell format type '{format_type}'") from None
1397
+ raise TypeError(f"unsuported cell format type '{format_type_name}'") from None
1301
1398
 
1302
1399
  cell = self._data[row][col]
1303
- if format_type == FormattingType.DATETIME and not isinstance(cell, DateCell):
1304
- type_name = type(cell).__name__
1305
- raise TypeError(f"cannot use date/time formatting for cells of type {type_name}")
1306
- elif not isinstance(cell, NumberCell) and not isinstance(cell, DateCell):
1307
- type_name = type(cell).__name__
1308
- raise TypeError(f"cannot set formatting for cells of type {type_name}")
1400
+ type_name = type(cell).__name__
1401
+ if type_name not in FORMATTING_ALLOWED_CELLS[format_type_name]:
1402
+ raise TypeError(
1403
+ f"cannot use {format_type_name} formatting for cells of type {type_name}"
1404
+ )
1309
1405
 
1310
1406
  format = Formatting(type=format_type, **kwargs)
1311
- format_id = self._model.format_archive(self._table_id, format_type, format)
1312
- cell._set_formatting(format_id, format_type)
1407
+ if format_type_name in FORMATTING_ACTION_CELLS:
1408
+ control_id = self._model.control_cell_archive(self._table_id, format_type, format)
1409
+ else:
1410
+ control_id = None
1411
+
1412
+ is_currency = True if format_type == FormattingType.CURRENCY else False
1413
+ if format_type_name in ["slider", "stepper"]:
1414
+ if "control_format" in kwargs:
1415
+ try:
1416
+ control_format = kwargs["control_format"].name
1417
+ number_format_type = FormattingType[control_format]
1418
+ is_currency = (
1419
+ True
1420
+ if kwargs["control_format"] == ControlFormattingType.CURRENCY
1421
+ else False
1422
+ )
1423
+ except (KeyError, AttributeError):
1424
+ raise TypeError(
1425
+ "unsupported number format '{control_format}' for format_type_name"
1426
+ ) from None
1427
+ else:
1428
+ number_format_type = FormattingType.NUMBER
1429
+ format_id = self._model.format_archive(self._table_id, number_format_type, format)
1430
+ elif format_type_name == "popup":
1431
+ popup_format_type = FormattingType.TEXT if isinstance(cell, TextCell) else True
1432
+ format_id = self._model.format_archive(self._table_id, popup_format_type, format)
1433
+ else:
1434
+ format_id = self._model.format_archive(self._table_id, format_type, format)
1435
+
1436
+ cell._set_formatting(format_id, format_type, control_id, is_currency=is_currency)
numbers_parser/file.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import os
3
+ import re
3
4
  from io import BytesIO
4
5
  from sys import version_info
5
6
  from zipfile import BadZipFile, ZipFile
@@ -32,11 +33,15 @@ def read_numbers_file(path, file_handler, object_handler=None):
32
33
  read_numbers_file(filepath, file_handler, object_handler)
33
34
  else:
34
35
  f = open(filepath, "rb")
35
- if filename.endswith(".iwa"):
36
- blob = f.read()
37
- extract_iwa_archives(blob, filepath, file_handler, object_handler)
38
36
  blob = f.read()
39
- file_handler(os.path.join(path, filename), blob)
37
+ if filename.endswith(".iwa"):
38
+ package_filepath = re.sub(r".*\.numbers/*", "", filepath)
39
+ extract_iwa_archives(blob, package_filepath, file_handler, object_handler)
40
+ else:
41
+ package_filepath = os.path.join(
42
+ re.sub(r".*\.numbers/*", "", path), filename
43
+ )
44
+ file_handler(package_filepath, blob)
40
45
  else:
41
46
  try:
42
47
  zipf = open_zipfile(path)
numbers_parser/model.py CHANGED
@@ -2,6 +2,7 @@ import math
2
2
  import re
3
3
  from array import array
4
4
  from collections import defaultdict
5
+ from hashlib import sha1
5
6
  from struct import pack
6
7
  from typing import Dict, List, Tuple, Union
7
8
  from warnings import warn
@@ -55,6 +56,7 @@ from numbers_parser.constants import (
55
56
  FORMAT_TYPE_MAP,
56
57
  MAX_TILE_SIZE,
57
58
  PACKAGE_ID,
59
+ CellInteractionType,
58
60
  FormatType,
59
61
  )
60
62
  from numbers_parser.containers import ObjectStore
@@ -219,6 +221,7 @@ class _NumbersModel(Cacheable):
219
221
  self._table_formats = DataLists(self, "format_table", "format")
220
222
  self._table_styles = DataLists(self, "styleTable", "reference")
221
223
  self._table_strings = DataLists(self, "stringTable", "string")
224
+ self._control_specs = DataLists(self, "control_cell_spec_table", "cell_spec")
222
225
  self._table_data = {}
223
226
  self._styles = None
224
227
  self._custom_formats = None
@@ -362,6 +365,64 @@ class _NumbersModel(Cacheable):
362
365
  format = TSKArchives.FormatStructArchive(**attrs)
363
366
  return self._table_formats.lookup_key(table_id, format)
364
367
 
368
+ def cell_popup_model(self, parent_id: int, format: Formatting):
369
+ tsce_items = [{"cell_value_type": "NIL_TYPE"}]
370
+ for item in format.popup_values:
371
+ if isinstance(item, str):
372
+ tsce_items.append(
373
+ {
374
+ "cell_value_type": "STRING_TYPE",
375
+ "string_value": {
376
+ "value": item,
377
+ "format": {"format_type": FormatType.TEXT},
378
+ },
379
+ }
380
+ )
381
+ else:
382
+ tsce_items.append(
383
+ {
384
+ "cell_value_type": "NUMBER_TYPE",
385
+ "number_value": {
386
+ "value": item,
387
+ "format": {"format_type": FormatType.DECIMAL},
388
+ },
389
+ }
390
+ )
391
+ popup_menu_id, _ = self.objects.create_object_from_dict(
392
+ f"Index/Tables/DataList-{parent_id}",
393
+ {"tsce_item": tsce_items},
394
+ TSTArchives.PopUpMenuModel,
395
+ True,
396
+ )
397
+ return popup_menu_id
398
+
399
+ def control_cell_archive(self, table_id: int, format_type: FormattingType, format: Formatting):
400
+ """Create control cell archive from a Formatting spec and return the table format ID"""
401
+ if format_type == FormattingType.TICKBOX:
402
+ cell_spec = TSTArchives.CellSpecArchive(interaction_type=CellInteractionType.TOGGLE)
403
+ elif format_type == FormattingType.RATING:
404
+ cell_spec = TSTArchives.CellSpecArchive(
405
+ interaction_type=CellInteractionType.RATING,
406
+ range_control_min=0.0,
407
+ range_control_max=5.0,
408
+ range_control_inc=1.0,
409
+ )
410
+ elif format_type == FormattingType.SLIDER:
411
+ cell_spec = TSTArchives.CellSpecArchive(
412
+ interaction_type=CellInteractionType.SLIDER,
413
+ range_control_min=format.minimum,
414
+ range_control_max=format.maximum,
415
+ range_control_inc=format.increment,
416
+ )
417
+ else: # POPUP
418
+ popup_id = self.cell_popup_model(self._control_specs.id(table_id), format)
419
+ cell_spec = TSTArchives.CellSpecArchive(
420
+ interaction_type=CellInteractionType.POPUP,
421
+ chooser_control_popup_model=TSPMessages.Reference(identifier=popup_id),
422
+ chooser_control_start_w_first=not (format.allow_none),
423
+ )
424
+ return self._control_specs.lookup_key(table_id, cell_spec)
425
+
365
426
  def add_custom_decimal_format_archive(self, format: CustomFormatting) -> None:
366
427
  """Create a custom format from the format spec"""
367
428
  integer_format = format.integer_format
@@ -1476,12 +1537,35 @@ class _NumbersModel(Cacheable):
1476
1537
  + str(cell.style.bg_color.g)
1477
1538
  + str(cell.style.bg_color.b)
1478
1539
  )
1540
+ if cell._style.bg_image is not None:
1541
+ fingerprint += cell._style.bg_image.filename
1479
1542
  if fingerprint not in cell_styles:
1480
1543
  cell_styles[fingerprint] = self.add_cell_style(cell._style)
1481
1544
  cell._style._cell_style_obj_id = cell_styles[fingerprint]
1482
1545
 
1483
1546
  def add_cell_style(self, style: Style) -> int:
1484
- if style.bg_color is not None:
1547
+ if style.bg_image is not None:
1548
+ datas = self.objects[PACKAGE_ID].datas
1549
+ image_id = self.next_image_identifier()
1550
+ datas.append(
1551
+ TSPArchiveMessages.DataInfo(
1552
+ identifier=image_id,
1553
+ digest=sha1(style.bg_image.data).digest(),
1554
+ preferred_file_name=style.bg_image.filename,
1555
+ file_name=style.bg_image.filename,
1556
+ materialized_length=len(style.bg_image.data),
1557
+ )
1558
+ )
1559
+ color_attrs = {
1560
+ "cell_fill": {
1561
+ "image": {
1562
+ "technique": "ScaleToFill",
1563
+ "imagedata": {"identifier": image_id},
1564
+ "interpretsUntaggedImageDataAsGeneric": False,
1565
+ }
1566
+ }
1567
+ }
1568
+ elif style.bg_color is not None:
1485
1569
  color_attrs = {
1486
1570
  "cell_fill": {
1487
1571
  "color": {
@@ -1530,6 +1614,7 @@ class _NumbersModel(Cacheable):
1530
1614
  style=TSPMessages.Reference(identifier=cell_style_id),
1531
1615
  )
1532
1616
  )
1617
+
1533
1618
  return cell_style_id
1534
1619
 
1535
1620
  def text_style_object_id(self, cell_storage) -> int:
@@ -1698,6 +1783,10 @@ class _NumbersModel(Cacheable):
1698
1783
  flags |= 0x200
1699
1784
  length += 4
1700
1785
  storage += pack("<i", cell._storage.formula_id)
1786
+ if cell._storage.control_id is not None:
1787
+ flags |= 0x400
1788
+ length += 4
1789
+ storage += pack("<i", cell._storage.control_id)
1701
1790
  if cell._storage.suggest_id is not None:
1702
1791
  flags |= 0x1000
1703
1792
  length += 4
@@ -2203,6 +2292,20 @@ class _NumbersModel(Cacheable):
2203
2292
  stroke_layer.stroke_runs.append(self.create_stroke(origin, length, border_value))
2204
2293
  layer_ids.append(TSPMessages.Reference(identifier=stroke_layer_id))
2205
2294
 
2295
+ def store_image(self, data: bytes, filename: str) -> None:
2296
+ """Store image data in the file store."""
2297
+ stored_filename = f"Data/{filename}"
2298
+ if stored_filename in self.objects.file_store:
2299
+ raise IndexError(f"{filename}: image already exists in document")
2300
+ self.objects.file_store[stored_filename] = data
2301
+
2302
+ def next_image_identifier(self):
2303
+ """Return the next available ID in the list of images in the document."""
2304
+ datas = self.objects[PACKAGE_ID].datas
2305
+ image_ids = [x.identifier for x in datas]
2306
+ # datas never appears to be an empty list (default themes include images)
2307
+ return max(image_ids) + 1
2308
+
2206
2309
 
2207
2310
  def rgb(obj) -> RGB:
2208
2311
  """Convert a TSPArchives.Color into an RGB tuple."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: numbers-parser
3
- Version: 4.8.1
3
+ Version: 4.9.0
4
4
  Summary: Read and write Apple Numbers spreadsheets
5
5
  Home-page: https://github.com/masaccio/numbers-parser
6
6
  License: MIT
@@ -187,8 +187,8 @@ Whilst support for writing numbers files has been stable since version
187
187
  and instead save data to a new file.
188
188
 
189
189
  Cell values are written using
190
- [Table.write()](https://masaccio.github.io/numbers-parser/#numbers_parser.Table.write)
191
- and `numbers-parser` will automatically create empty rows and columns
190
+ [Table.write()](https://masaccio.github.io/numbers-parser/api/table.html#numbers_parser.Table.write) and
191
+ `numbers-parser` will automatically create empty rows and columns
192
192
  for any cell references that are out of range of the current table.
193
193
 
194
194
  ```python
@@ -201,12 +201,9 @@ table.write("B7", datetime(2020, 12, 25))
201
201
  doc.save("new-sheet.numbers")
202
202
  ```
203
203
 
204
- Additional tables and worksheets can be added to a `Document` before
205
- saving using
206
- [Document.add_sheet()](https://masaccio.github.io/numbers-parser/#numbers_parser.Document.add_sheet)
207
- and
208
- [Sheet.add_table()](https://masaccio.github.io/numbers-parser/#numbers_parser.Sheet.add_table)
209
- respectively:
204
+ Additional tables and worksheets can be added to a `Document` before saving using
205
+ [Document.add_sheet()](https://masaccio.github.io/numbers-parser/api/document.html#numbers_parser.Document.add_sheet) and
206
+ [Sheet.add_table()](https://masaccio.github.io/numbers-parser/api/sheet.html#numbers_parser.Sheet.add_table) respectively:
210
207
 
211
208
  ```python
212
209
  doc = Document()
@@ -222,27 +219,28 @@ doc.save("sheet.numbers")
222
219
  ### Styles
223
220
 
224
221
  `numbers_parser` currently only supports paragraph styles and cell
225
- styles. The following paragraph styles are supported:
222
+ styles. The following styles are supported:
226
223
 
227
224
  - font attributes: bold, italic, underline, strikethrough
228
225
  - font selection and size
229
226
  - text foreground color
230
227
  - horizontal and vertical alignment
231
228
  - cell background color
229
+ - cell background images
232
230
  - cell indents (first line, left, right, and text inset)
233
231
 
234
232
  Numbers conflates style attributes that can be stored in paragraph
235
233
  styles (the style menu in the text panel) with the settings that are
236
234
  available on the Style tab of the Text panel. Some attributes in Numbers
237
- are not applied to new cells when a style is applied. To keep the API
238
- simple, `numbers-parser` packs all styling into a single
239
- [Style](https://masaccio.github.io/numbers-parser/api/style.html)
240
- object. When a document is saved, the attributes not stored in a
241
- paragraph style are applied to each cell that includes it.
235
+ are not applied to new cells when a style is applied.
236
+
237
+ To keep the API simple, `numbers-parser` packs all styling into a single
238
+ [Style](https://masaccio.github.io/numbers-parser/api/style.html) object. When a document is saved, the attributes
239
+ not stored in a paragraph style are applied to each cell that includes it.
242
240
 
243
241
  Styles are read from cells using the
244
- [Cell.style](https://masaccio.github.io/numbers-parser/api/cells.html#numbers_parser.Cell.style)
245
- property and you can add new styles with
242
+ [Cell.style](https://masaccio.github.io/numbers-parser/api/cells.html#numbers_parser.Cell.style) property and you can
243
+ add new styles with
246
244
  [Document.add_style](https://masaccio.github.io/numbers-parser/api/document.html#numbers_parser.Document.add_style).
247
245
 
248
246
  ```python
@@ -272,7 +270,7 @@ internally by the package. Changing a data format for cell has no impact
272
270
  on any other cells.
273
271
 
274
272
  Cell formats are changed using
275
- [Table.set_cell_formatting](https://masaccio.github.io/numbers-parser/#numbers_parser.Table.set_cell_formatting):
273
+ [Table.set_cell_formatting()](https://masaccio.github.io/numbers-parser/api/table.html#numbers_parser.Table.set_cell_formatting):
276
274
 
277
275
  ```python
278
276
  table.set_cell_formatting(
@@ -293,9 +291,9 @@ Custom formats are shared across a Document and can be applied to
293
291
  multiple cells in multiple tables. Editing a custom format changes the
294
292
  appearance of data in all cells that share that format. You must first
295
293
  add a custom format to the document using
296
- [Document.add_custom_format](https://masaccio.github.io/numbers-parser/#numbers_parser.Document.add_custom_format)
294
+ [Document.add_custom_format()](https://masaccio.github.io/numbers-parser/api/document.html#numbers_parser.Document.add_custom_format)
297
295
  before assigning it to cells using
298
- [Table.set_cell_formatting](https://masaccio.github.io/numbers-parser/#numbers_parser.Table.set_cell_formatting):
296
+ [Table.set_cell_formatting()](https://masaccio.github.io/numbers-parser/api/table.html#numbers_parser.Table.set_cell_formatting):
299
297
 
300
298
  ```python
301
299
  long_date = doc.add_custom_format(
@@ -322,13 +320,11 @@ table to allow for drawing borders across multiple cells. Setting the
322
320
  border of merged cells is not possible unless the edge of the cells is
323
321
  at the end of the merged region.
324
322
 
325
- Borders are represented using the
326
- [Border](https://masaccio.github.io/numbers-parser/#numbers_parser.Border)
327
- class that can be initialized with line width, color and line style. The
323
+ Borders are represented using the [Border](https://masaccio.github.io/numbers-parser/api/border.html) class
324
+ that can be initialized with line width, color and line style. The
328
325
  current state of a cell border is read using the
329
- [Cell.border](https://masaccio.github.io/numbers-parser/#numbers_parser.Cell.border)
330
- property. The
331
- [Table.set_cell_border](https://masaccio.github.io/numbers-parser/#numbers_parser.Table.set_cell_border)
326
+ [Cell.border](https://masaccio.github.io/numbers-parser/api/cells.html#numbers_parser.Cell.border) property
327
+ and [Table.set_cell_border()](https://masaccio.github.io/numbers-parser/api/table.html#numbers_parser.Table.set_cell_border)
332
328
  sets the border for a cell edge or a range of cells.
333
329
 
334
330
  ## API
@@ -400,6 +396,5 @@ Current known limitations of `numbers-parser` are:
400
396
 
401
397
  ## License
402
398
 
403
- All code in this repository is licensed under the [MIT
404
- License](https://github.com/masaccio/numbers-parser/blob/master/LICENSE.rst)
399
+ All code in this repository is licensed under the [MIT License](https://github.com/masaccio/numbers-parser/blob/master/LICENSE.rst).
405
400
 
@@ -2,16 +2,16 @@ numbers_parser/__init__.py,sha256=hyoQ4x-1E8yDBMR128kZhSDLCfrnjNdz4_dEpKlekk4,19
2
2
  numbers_parser/_cat_numbers.py,sha256=-HboBJT11Vjcr8sjLZl7Z6qAapnPEc_kFYq6PTqON20,4619
3
3
  numbers_parser/_unpack_numbers.py,sha256=zfpBOfM92rMHSRQVR4tExf0fWI2Lbbb6WrwQPoq4gMo,5689
4
4
  numbers_parser/bullets.py,sha256=OnVVMPjhTDrC-ncw52Gb00UEXNmn2Rvd3xi7lfqW3hk,2616
5
- numbers_parser/cell.py,sha256=zXcxN1vtjZnE4jT2mc2bi5eqSrCsmbsx9G5wbr9rAuM,36384
6
- numbers_parser/cell_storage.py,sha256=_anyiHnG-pAZ9OTSdisT7lted9PGSzQcn_-xpsJVU0o,32873
7
- numbers_parser/constants.py,sha256=XFBGGFEHToU-DDrNxhtDsmqd4P0Yc6KiNdQAxzFCWqg,7477
8
- numbers_parser/containers.py,sha256=b9sOCBd60YjMxJ-s9u-erWusA3guHU-c1RAUqM8CyKs,4191
5
+ numbers_parser/cell.py,sha256=YfHs1xxVSNY_oEodTN3IhqdO5tUhdgzZz5Z0m10dtpw,38457
6
+ numbers_parser/cell_storage.py,sha256=jaGpleFg8xWPT3U-O4bczAMYFR54t3x920UBKcrUiOQ,34354
7
+ numbers_parser/constants.py,sha256=BJGNz0ZZCs5xfAXlzLox4tvKOHIpDMXgJQ6i5yvdr4A,9606
8
+ numbers_parser/containers.py,sha256=yR_T2yF5QiVj7Dg22nCMLvo___Xrec3j8kitbxiaWyU,4220
9
9
  numbers_parser/currencies.py,sha256=8k4a3WKmDoHeurkDICymHX13N7ManHSTaka_JNXCZYA,3767
10
10
  numbers_parser/data/empty.numbers,sha256=8JOp035V-p2ff9_Wao7mLcYvb6_if6O2cus_esjVA9k,90316
11
- numbers_parser/document.py,sha256=bjF0kjKh-Z2Z6H1Uwn1NVwx7b1URqNOXrjd60f4GjiI,49346
11
+ numbers_parser/document.py,sha256=JmGyafm10FXnS5zmXkMTRVvYM-KnCKtcmbNCwMDyuSU,55333
12
12
  numbers_parser/exceptions.py,sha256=G8dASUQZI8ksHYRVfdGWJzgsJD5CBpcZvmDJUZTqT-c,670
13
13
  numbers_parser/experimental.py,sha256=WARjTa-2ePb8Ga8Q6oDP6EJCs12ofLRF2YpwzUu66ZI,374
14
- numbers_parser/file.py,sha256=ivcl5326TQXwj1GP9FCh-WiM_oXZumgQdByehAEw1f8,3966
14
+ numbers_parser/file.py,sha256=buNbZRzQCIlr7H4JxwTh2_eh7oDA2fGH5ZiFpIEVHoo,4200
15
15
  numbers_parser/formula.py,sha256=JRsG0L21wS70oJ-FB46Amyoy-sKizWb-iUhSXUcVJ-U,10572
16
16
  numbers_parser/generated/TNArchives_pb2.py,sha256=txkTtPHvdXVvv7zO1dHCxxnixaFulK7hJVLQrH3cIJc,16007
17
17
  numbers_parser/generated/TNArchives_sos_pb2.py,sha256=AYI1X5t5Gb4l941jXlHEY0v97ToIze0bSYBR7KQmS0A,1215
@@ -50,11 +50,11 @@ numbers_parser/generated/fontmap.py,sha256=pqc1HwwTr3UbFMmhUaHJg1dX5-3pXbyhfS2bk
50
50
  numbers_parser/generated/functionmap.py,sha256=VdZo0ERMYONcrnJFwABcSCHb8pjA4wY2ogt8Janz57M,6082
51
51
  numbers_parser/iwafile.py,sha256=MuFIlB_hdXTTZflxoX_ZvA_68OaJkmRQ4eJ2UAiCKXQ,11833
52
52
  numbers_parser/mapping.py,sha256=in8W3S8DmTcPefFaxnATLw0FQ4YnFsnAE-cl5dljzJE,32215
53
- numbers_parser/model.py,sha256=ngC89eluYihQ-cRnqSKsZGn2LUNHyaEOneXploSG_po,98754
53
+ numbers_parser/model.py,sha256=qvU0RRF4YgUcfhtRlDdRP09gl8rUY0GNgeHpOTJYvHk,103312
54
54
  numbers_parser/numbers_cache.py,sha256=1ghEBghQAYFpPiEeOtb74i016mXc039v1pOubbqvaLs,1141
55
55
  numbers_parser/numbers_uuid.py,sha256=-LeAj_ULC0va57pEmyegGY0xXqkNNjyuLukCaiQJhOk,2642
56
- numbers_parser-4.8.1.dist-info/LICENSE.rst,sha256=8vTa1-5KSdHrTpU9rlheO5005EWReEPMpjV7BjSaMc4,1050
57
- numbers_parser-4.8.1.dist-info/METADATA,sha256=k97XRQw49EhuCV3iHQG9tQ853HP8gnGISWgO93-F-Jk,16017
58
- numbers_parser-4.8.1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
59
- numbers_parser-4.8.1.dist-info/entry_points.txt,sha256=V91uB9vBPxf3eCY1h-0syv21imYCT0MJfMxf87DmwIk,115
60
- numbers_parser-4.8.1.dist-info/RECORD,,
56
+ numbers_parser-4.9.0.dist-info/LICENSE.rst,sha256=8vTa1-5KSdHrTpU9rlheO5005EWReEPMpjV7BjSaMc4,1050
57
+ numbers_parser-4.9.0.dist-info/METADATA,sha256=RWBbYXp1YHBV7Jj4Q6C_RwXIA_-JEi55l6o1A7G9r4w,16152
58
+ numbers_parser-4.9.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
59
+ numbers_parser-4.9.0.dist-info/entry_points.txt,sha256=V91uB9vBPxf3eCY1h-0syv21imYCT0MJfMxf87DmwIk,115
60
+ numbers_parser-4.9.0.dist-info/RECORD,,