diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to .github/CODE_OF_CONDUCT.md diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md diff --git a/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from PULL_REQUEST_TEMPLATE.md rename to .github/PULL_REQUEST_TEMPLATE.md diff --git a/excelize.py b/excelize.py index a8e45a2..af13994 100644 --- a/excelize.py +++ b/excelize.py @@ -219,7 +219,8 @@ def c_value_to_py(ctypes_instance, py_instance): for i in range(l): py_list.append( c_value_to_py( - c_array[i], get_args(py_field_args[0])[0]() + c_array[i], + get_args(py_field_args[0])[0](), ), ) setattr(py_instance, py_field_name, py_list) @@ -419,7 +420,8 @@ def py_value_to_c_interface(py_value): bool: lambda: Interface(type=4, boolean=py_value), datetime: lambda: Interface(type=5, integer=int(py_value.timestamp())), date: lambda: Interface( - type=5, integer=int(datetime.combine(py_value, time.min).timestamp()) + type=5, + integer=int(datetime.combine(py_value, time.min).timestamp()), ), } interface = type_mappings.get(type(py_value), lambda: Interface())() @@ -699,7 +701,8 @@ def add_pivot_table(self, opts: Optional[PivotTableOptions]) -> Optional[Excepti """ lib.AddPivotTable.restype = c_char_p err = lib.AddPivotTable( - self.file_index, byref(py_value_to_c(opts, types_go._PivotTableOptions())) + self.file_index, + byref(py_value_to_c(opts, types_go._PivotTableOptions())), ).decode(ENCODE) return None if err == "" else Exception(err) @@ -1126,7 +1129,9 @@ def get_active_sheet_index(self) -> int: res = lib.GetActiveSheetIndex(self.file_index) return res - def get_app_props(self) -> Tuple[Optional[AppProperties], Optional[Exception]]: + def get_app_props( + self, + ) -> Tuple[Optional[AppProperties], Optional[Exception]]: """ Get document application properties. @@ -1138,7 +1143,7 @@ def get_app_props(self) -> Tuple[Optional[AppProperties], Optional[Exception]]: lib.GetAppProps.restype = types_go._GetAppPropsResult res = lib.GetAppProps(self.file_index) err = res.err.decode(ENCODE) - return c_value_to_py(res.opts, AppProperties()) if err == "" else None, ( + return (c_value_to_py(res.opts, AppProperties()) if err == "" else None), ( None if err == "" else Exception(err) ) @@ -1271,6 +1276,50 @@ def get_rows( return rows, None if err == "" else Exception(err) + def get_style(self, style_id: int) -> Tuple[Optional[Style], Optional[Exception]]: + """ + Get style definition by given style index. + + Args: + style_id (int): The style ID + + Returns: + Tuple[Optional[Style], Optional[Exception]]: A tuple containing the + Style object if found, otherwise None, and an Exception object if an + error occurred, otherwise None. + """ + lib.GetStyle.restype = types_go._GetStyleResult + res = lib.GetStyle(self.file_index, c_int(style_id)) + err = res.err.decode(ENCODE) + if err == "": + return c_value_to_py(res.style, Style()), None + return None, Exception(err) + + def merge_cell( + self, sheet: str, top_left_cell: str, bottom_right_cell: str + ) -> Optional[Exception]: + """ + Merge cells by given range reference and sheet name. Merging cells only + keeps the upper-left cell value, and discards the other values. + + Args: + sheet (str): The worksheet name + top_left_cell (str): The top-left cell reference + bottom_right_cell (str): The right-bottom cell reference + + Returns: + Optional[Exception]: Returns None if no error occurred, + otherwise returns an Exception with the message. + """ + lib.MergeCell.restype = c_char_p + err = lib.MergeCell( + self.file_index, + sheet.encode(ENCODE), + top_left_cell.encode(ENCODE), + bottom_right_cell.encode(ENCODE), + ).decode(ENCODE) + return None if err == "" else Exception(err) + def new_sheet(self, sheet: str) -> Tuple[int, Optional[Exception]]: """ Create a new sheet by given a worksheet name and returns the index of @@ -1309,25 +1358,6 @@ def new_style(self, style: Style) -> Tuple[int, Optional[Exception]]: err = res.err.decode(ENCODE) return res.style, None if err == "" else Exception(err) - def get_style(self, style_id: int) -> Tuple[Optional[Style], Optional[Exception]]: - """ - Get style definition by given style index. - - Args: - style_id (int): The style ID - - Returns: - Tuple[Optional[Style], Optional[Exception]]: A tuple containing the - Style object if found, otherwise None, and an Exception object if an - error occurred, otherwise None. - """ - lib.GetStyle.restype = types_go._GetStyleResult - res = lib.GetStyle(self.file_index, c_int(style_id)) - err = res.err.decode(ENCODE) - if err == "": - return c_value_to_py(res.style, Style()), None - return None, Exception(err) - def set_active_sheet(self, index: int) -> Optional[Exception]: """ Set the default active sheet of the workbook by a given index. Note that @@ -1384,7 +1414,12 @@ def set_cell_formula( return None if err == "" else Exception(err) def set_cell_hyperlink( - self, sheet: str, cell: str, link: str, link_type: str, *opts: HyperlinkOpts + self, + sheet: str, + cell: str, + link: str, + link_type: str, + *opts: HyperlinkOpts, ) -> Optional[Exception]: """ Set cell hyperlink by given worksheet name and link URL address. The @@ -1449,7 +1484,11 @@ def set_cell_hyperlink( return None if err == "" else Exception(err) def set_cell_style( - self, sheet: str, top_left_cell: str, bottom_right_cell: str, style_id: int + self, + sheet: str, + top_left_cell: str, + bottom_right_cell: str, + style_id: int, ) -> Optional[Exception]: """ Add style attribute for cells by given worksheet name, range reference @@ -1479,7 +1518,10 @@ def set_cell_style( return None if err == "" else Exception(err) def set_cell_value( - self, sheet: str, cell: str, value: Union[None, int, str, bool, datetime, date] + self, + sheet: str, + cell: str, + value: Union[None, int, str, bool, datetime, date], ) -> Optional[Exception]: """ Set the value of a cell. The specified coordinates should not be in the @@ -1511,6 +1553,28 @@ def set_cell_value( ).decode(ENCODE) return None if err == "" else Exception(err) + def set_sheet_background(self, sheet: str, picture: str) -> Optional[Exception]: + """ + Set background picture by given worksheet name and file path. Supported + image types: BMP, EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, + and WMZ. + + Args: + sheet (str): The worksheet name + picture (str): The image file path + + Returns: + Optional[Exception]: Returns None if no error occurred, + otherwise returns an Exception with the message. + """ + lib.SetSheetBackground.restype = c_char_p + err = lib.SetSheetBackground( + self.file_index, + sheet.encode(ENCODE), + picture.encode(ENCODE), + ).decode(ENCODE) + return None if err == "" else Exception(err) + def set_sheet_background_from_bytes( self, sheet: str, extension: str, picture: bytes ) -> Optional[Exception]: @@ -1683,3 +1747,27 @@ def open_file( if err == "": return File(res.idx), None return None, Exception(err) + + +def open_reader( + buffer: bytes, *opts: Options +) -> Tuple[Optional[File], Optional[Exception]]: + """ + Read data stream from bytes and return a populated spreadsheet file. + + Args: + buffer (bytes): The contents buffer of the file + *opts (Options): Optional parameters for opening the file. + + Returns: + Tuple[Optional[File], Optional[Exception]]: A tuple containing a File + object if successful, or None and an Exception if an error occurred. + """ + lib.OpenReader.restype, options = types_go._OptionsResult, None + if len(opts) > 0: + options = byref(py_value_to_c(opts[0], types_go._Options())) + res = lib.OpenReader(cast(buffer, POINTER(c_ubyte)), len(buffer), options) + err = res.err.decode(ENCODE) + if err == "": + return File(res.idx), None + return None, Exception(err) diff --git a/main.go b/main.go index fb86d0f..af87873 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ package main import "C" import ( + "bytes" "errors" "reflect" "sync" @@ -1072,6 +1073,22 @@ func GetStyle(idx, styleID int) C.struct_GetStyleResult { return C.struct_GetStyleResult{style: cVal.Elem().Interface().(C.struct_Style), err: C.CString(errNil)} } +// MergeCell provides a function to merge cells by given range reference and +// sheet name. Merging cells only keeps the upper-left cell value, and +// discards the other values. +// +//export MergeCell +func MergeCell(idx int, sheet, topLeftCell, bottomRightCell *C.char) *C.char { + f, ok := files.Load(idx) + if !ok { + return C.CString("") + } + if err := f.(*excelize.File).MergeCell(C.GoString(sheet), C.GoString(topLeftCell), C.GoString(bottomRightCell)); err != nil { + return C.CString(err.Error()) + } + return C.CString(errNil) +} + // NewFile provides a function to create new file by default template. // //export NewFile @@ -1153,6 +1170,34 @@ func OpenFile(filename *C.char, opts *C.struct_Options) C.struct_OptionsResult { return C.struct_OptionsResult{idx: C.int(idx), err: C.CString(errNil)} } +// OpenReader read data stream from io.Reader and return a populated spreadsheet +// file. +// +//export OpenReader +func OpenReader(b *C.uchar, bLen C.int, opts *C.struct_Options) C.struct_OptionsResult { + var options excelize.Options + if opts != nil { + goVal, err := cValueToGo(reflect.ValueOf(*opts), reflect.TypeOf(excelize.Options{})) + if err != nil { + return C.struct_OptionsResult{idx: C.int(-1), err: C.CString(err.Error())} + } + options = goVal.Elem().Interface().(excelize.Options) + } + buf := C.GoBytes(unsafe.Pointer(b), bLen) + f, err := excelize.OpenReader(bytes.NewReader(buf), options) + if err != nil { + return C.struct_OptionsResult{idx: C.int(-1), err: C.CString(err.Error())} + } + var idx int + files.Range(func(_, _ interface{}) bool { + idx++ + return true + }) + idx++ + files.Store(idx, f) + return C.struct_OptionsResult{idx: C.int(idx), err: C.CString(errNil)} +} + // Save provides a function to override the spreadsheet with origin path. // //export Save @@ -1325,6 +1370,22 @@ func SetCellValue(idx int, sheet, cell *C.char, value *C.struct_Interface) *C.ch return C.CString(errNil) } +// SetSheetBackground provides a function to set background picture by given +// worksheet name and file path. Supported image types: BMP, EMF, EMZ, GIF, +// JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. +// +//export SetSheetBackground +func SetSheetBackground(idx int, sheet, picture *C.char) *C.char { + f, ok := files.Load(idx) + if !ok { + return C.CString(errFilePtr) + } + if err := f.(*excelize.File).SetSheetBackground(C.GoString(sheet), C.GoString(picture)); err != nil { + C.CString(err.Error()) + } + return C.CString(errNil) +} + // SetSheetBackgroundFromBytes provides a function to set background picture by // given worksheet name, extension name and image data. Supported image types: // BMP, EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. diff --git a/test_excelize.py b/test_excelize.py index 161770a..037454b 100644 --- a/test_excelize.py +++ b/test_excelize.py @@ -59,7 +59,7 @@ def test_py_value_to_c(self): def test_open_file(self): f, err = excelize.open_file("Book1.xlsx") self.assertIsNone(f) - self.assertTrue(err.__str__().startswith("open Book1.xlsx")) + self.assertTrue(str(err).startswith("open Book1.xlsx")) def test_app_props(self): f = excelize.new_file() @@ -113,12 +113,12 @@ def test_style(self): self.assertEqual(style, s) self.assertIsNone(f.set_cell_style("Sheet1", "A1", "B2", style_id)) self.assertEqual( - f.set_cell_style("SheetN", "A1", "B2", style_id).__str__(), + str(f.set_cell_style("SheetN", "A1", "B2", style_id)), "sheet SheetN does not exist", ) style, err = f.get_style(2) - self.assertEqual("invalid style ID 2", err.__str__()) + self.assertEqual("invalid style ID 2", str(err)) self.assertIsNone(style) self.assertIsNone(f.save_as("TestStyle.xlsx")) self.assertIsNone( @@ -145,7 +145,7 @@ def test_style(self): ) self.assertIsNone(f.set_cell_value("Sheet1", "A8", datetime.date(2016, 8, 30))) self.assertEqual( - f.set_cell_value("SheetN", "A9", None).__str__(), + str(f.set_cell_value("SheetN", "A9", None)), "sheet SheetN does not exist", ) val, err = f.get_cell_value("Sheet1", "A2") @@ -169,42 +169,46 @@ def test_style(self): self.assertIsNone(f.duplicate_row("Sheet1", 9)) self.assertIsNone(f.duplicate_row_to("Sheet1", 10, 10)) + self.assertIsNone(f.merge_cell("Sheet1", "A1", "B2")) + idx, err = f.new_sheet("Sheet2") self.assertEqual(idx, 1) self.assertIsNone(err) self.assertIsNone(f.set_active_sheet(idx)) self.assertEqual(f.get_active_sheet_index(), idx) + self.assertIsNone(f.set_sheet_background("Sheet2", "chart.png")) + idx, err = f.new_sheet(":\\/?*[]Maximum 31 characters allowed in sheet title.") self.assertEqual(idx, -1) self.assertEqual( - err.__str__(), "the sheet name length exceeds the 31 characters limit" + str(err), "the sheet name length exceeds the 31 characters limit" ) idx, err = f.new_sheet("Sheet3") self.assertIsNone(err) self.assertIsNone(f.copy_sheet(1, idx)) - self.assertEqual(f.copy_sheet(1, 4).__str__(), "invalid worksheet index") + self.assertEqual(str(f.copy_sheet(1, 4)), "invalid worksheet index") self.assertIsNone(f.delete_sheet("Sheet3")) self.assertEqual( - f.delete_sheet("Sheet:1").__str__(), + str(f.delete_sheet("Sheet:1")), "the sheet can not contain any of the characters :\\/?*[or]", ) self.assertEqual( - f.delete_chart("SheetN", "A1").__str__(), "sheet SheetN does not exist" + str(f.delete_chart("SheetN", "A1")), "sheet SheetN does not exist" ) self.assertEqual( - f.delete_comment("SheetN", "A1").__str__(), "sheet SheetN does not exist" + str(f.delete_comment("SheetN", "A1")), "sheet SheetN does not exist" ) self.assertIsNone(f.delete_picture("Sheet1", "A1")) self.assertEqual( - f.delete_comment("SheetN", "A1").__str__(), "sheet SheetN does not exist" + str(f.delete_comment("SheetN", "A1")), "sheet SheetN does not exist" ) - self.assertEqual(f.delete_slicer("x").__str__(), "slicer x does not exist") + self.assertEqual(str(f.delete_slicer("x")), "slicer x does not exist") rows, err = f.get_rows("Sheet1") self.assertIsNone(err) @@ -237,6 +241,15 @@ def test_style(self): self.assertIsNone(f.save(excelize.Options(password=""))) self.assertIsNone(f.close()) + with open("TestStyle.xlsx", "rb") as file: + f, err = excelize.open_reader(file.read()) + self.assertIsNone(err) + self.assertIsNone(f.save_as("TestOpenReader.xlsx")) + + with open("chart.png", "rb") as file: + _, err = excelize.open_reader(file.read(), excelize.Options(password="")) + self.assertEqual(str(err), "zip: not a valid zip file") + def test_add_chart(self): f = excelize.new_file() for idx, row in enumerate( @@ -644,7 +657,7 @@ def test_cell_name_to_coordinates(self): self.assertEqual(col, -1) self.assertEqual(row, -1) self.assertEqual( - err.__str__(), + str(err), 'cannot convert cell "A" to coordinates: invalid cell name "A"', ) @@ -683,7 +696,7 @@ def test_column_name_to_number(self): col, err = excelize.column_name_to_number("-") self.assertEqual(col, -1) self.assertEqual( - err.__str__(), + str(err), 'invalid column name "-"', ) @@ -695,7 +708,7 @@ def test_column_number_to_name(self): name, err = excelize.column_number_to_name(0) self.assertEqual(name, "") self.assertEqual( - err.__str__(), + str(err), "the column number must be greater than or equal to 1 and less than or equal to 16384", )