From 87df446d49abab5a9147b1081c39c3dd36a70c3b Mon Sep 17 00:00:00 2001 From: John McNamara Date: Fri, 10 Jan 2020 21:46:34 +0000 Subject: [PATCH] Added support for cell comments in constant_memory mode. Added support for cell comments in constant_memory mode and also refactored the internal data structure for handling comments. Issue #38 --- include/xlsxwriter/worksheet.h | 3 +- src/worksheet.c | 155 +++++++++++++-------- test/functional/src/test_comment16.c | 30 ++++ test/functional/src/test_comment56.c | 41 ++++++ test/functional/src/test_optimize13.c | 25 ++++ test/functional/src/test_optimize14.c | 32 +++++ test/functional/test_comment.py | 6 + test/functional/test_optimize.py | 6 + test/functional/xlsx_files/comment16.xlsx | Bin 0 -> 9085 bytes test/functional/xlsx_files/optimize13.xlsx | Bin 0 -> 6375 bytes test/functional/xlsx_files/optimize14.xlsx | Bin 0 -> 6412 bytes 11 files changed, 241 insertions(+), 57 deletions(-) create mode 100644 test/functional/src/test_comment16.c create mode 100644 test/functional/src/test_comment56.c create mode 100644 test/functional/src/test_optimize13.c create mode 100644 test/functional/src/test_optimize14.c create mode 100644 test/functional/xlsx_files/comment16.xlsx create mode 100644 test/functional/xlsx_files/optimize13.xlsx create mode 100644 test/functional/xlsx_files/optimize14.xlsx diff --git a/include/xlsxwriter/worksheet.h b/include/xlsxwriter/worksheet.h index 2264f27d..5db18041 100644 --- a/include/xlsxwriter/worksheet.h +++ b/include/xlsxwriter/worksheet.h @@ -227,6 +227,7 @@ enum cell_types { ARRAY_FORMULA_CELL, BLANK_CELL, BOOLEAN_CELL, + COMMENT, HYPERLINK_URL, HYPERLINK_INTERNAL, HYPERLINK_EXTERNAL @@ -898,6 +899,7 @@ typedef struct lxw_worksheet { FILE *optimize_tmpfile; struct lxw_table_rows *table; struct lxw_table_rows *hyperlinks; + struct lxw_table_rows *comments; struct lxw_cell **array; struct lxw_merged_ranges *merged_ranges; struct lxw_selections *selections; @@ -1061,7 +1063,6 @@ typedef struct lxw_row { uint8_t row_changed; uint8_t data_changed; uint8_t height_changed; - uint8_t has_comments; struct lxw_table_cells *cells; diff --git a/src/worksheet.c b/src/worksheet.c index f45d2e53..ac0deb65 100644 --- a/src/worksheet.c +++ b/src/worksheet.c @@ -87,9 +87,14 @@ lxw_worksheet_new(lxw_worksheet_init_data *init_data) GOTO_LABEL_ON_MEM_ERROR(worksheet->hyperlinks, mem_error); RB_INIT(worksheet->hyperlinks); + worksheet->comments = calloc(1, sizeof(struct lxw_table_rows)); + GOTO_LABEL_ON_MEM_ERROR(worksheet->comments, mem_error); + RB_INIT(worksheet->comments); + /* Initialize the cached rows. */ worksheet->table->cached_row_num = LXW_ROW_MAX + 1; worksheet->hyperlinks->cached_row_num = LXW_ROW_MAX + 1; + worksheet->comments->cached_row_num = LXW_ROW_MAX + 1; if (init_data && init_data->optimize) { worksheet->array = calloc(LXW_COL_MAX, sizeof(struct lxw_cell *)); @@ -402,6 +407,18 @@ lxw_worksheet_free(lxw_worksheet *worksheet) free(worksheet->hyperlinks); } + if (worksheet->comments) { + for (row = RB_MIN(lxw_table_rows, worksheet->comments); row; + row = next_row) { + + next_row = RB_NEXT(lxw_table_rows, worksheet->comments, row); + RB_REMOVE(lxw_table_rows, worksheet->comments, row); + _free_row(row); + } + + free(worksheet->comments); + } + if (worksheet->merged_ranges) { while (!STAILQ_EMPTY(worksheet->merged_ranges)) { merged_range = STAILQ_FIRST(worksheet->merged_ranges); @@ -432,7 +449,7 @@ lxw_worksheet_free(lxw_worksheet *worksheet) free(worksheet->chart_data); } - /* Just free the list. The list objects are free elsewhere. */ + /* Just free the list. The list objects are freed from the RB tree. */ free(worksheet->comment_objs); if (worksheet->selections) { @@ -700,6 +717,24 @@ _new_boolean_cell(lxw_row_t row_num, lxw_col_t col_num, int value, return cell; } +/* + * Create a new comment cell object. + */ +STATIC lxw_cell * +_new_comment_cell(lxw_row_t row_num, lxw_col_t col_num, + lxw_vml_obj *comment_obj) +{ + lxw_cell *cell = calloc(1, sizeof(lxw_cell)); + RETURN_ON_MEM_ERROR(cell, cell); + + cell->row_num = row_num; + cell->col_num = col_num; + cell->type = COMMENT; + cell->comment = comment_obj; + + return cell; +} + /* * Create a new worksheet hyperlink cell object. */ @@ -795,7 +830,6 @@ _insert_cell_list(struct lxw_table_cells *cell_list, /* If existing_cell is not NULL, then that cell already existed. */ /* Remove existing_cell and add new one in again. */ if (existing_cell) { - existing_cell->comment = NULL; RB_REMOVE(lxw_table_cells, cell_list, existing_cell); /* Add it in again. */ @@ -814,7 +848,6 @@ _insert_cell(lxw_worksheet *self, lxw_row_t row_num, lxw_col_t col_num, lxw_cell *cell) { lxw_row *row = _get_row(self, row_num); - lxw_vml_obj *existing_comment = NULL; if (!self->optimize) { row->data_changed = LXW_TRUE; @@ -825,20 +858,47 @@ _insert_cell(lxw_worksheet *self, lxw_row_t row_num, lxw_col_t col_num, row->data_changed = LXW_TRUE; /* Overwrite an existing cell if necessary. */ - if (self->array[col_num]) { - existing_comment = self->array[col_num]->comment; - self->array[col_num]->comment = NULL; + if (self->array[col_num]) _free_cell(self->array[col_num]); - } self->array[col_num] = cell; - self->array[col_num]->comment = existing_comment; } } } /* - * Insert a hyperlink object into the hyperlink list. + * Insert a blank placeholder cell in the cells RB tree in the same position + * as a comment so that the rows "spans" calculation is correct. Since the + * blank cell doesn't have a format it is ignored when writing. If there is + * already a cell in the required position we don't have add a new cell. + */ +STATIC void +_insert_cell_placeholder(lxw_worksheet *self, lxw_row_t row_num, + lxw_col_t col_num) +{ + lxw_row *row; + lxw_cell *cell; + + /* The spans calculation isn't required in constant_memory mode. */ + if (self->optimize) + return; + + cell = _new_blank_cell(row_num, col_num, NULL); + if (!cell) + return; + + /* Only add a cell if one doesn't already exist. */ + row = _get_row(self, row_num); + if (!RB_FIND(lxw_table_cells, row->cells, cell)) { + _insert_cell_list(row->cells, cell, col_num); + } + else { + _free_cell(cell); + } +} + +/* + * Insert a hyperlink object into the hyperlink RB tree. */ STATIC void _insert_hyperlink(lxw_worksheet *self, lxw_row_t row_num, lxw_col_t col_num, @@ -849,6 +909,18 @@ _insert_hyperlink(lxw_worksheet *self, lxw_row_t row_num, lxw_col_t col_num, _insert_cell_list(row->cells, link, col_num); } +/* + * Insert a comment into the comment RB tree. + */ +STATIC void +_insert_comment(lxw_worksheet *self, lxw_row_t row_num, lxw_col_t col_num, + lxw_cell *link) +{ + lxw_row *row = _get_row_list(self->comments, row_num); + + _insert_cell_list(row->cells, link, col_num); +} + /* * Next power of two for column reallocs. Taken from bithacks in the public * domain. @@ -2155,7 +2227,7 @@ _get_comment_params(lxw_vml_obj *comment, lxw_comment_options *options) /* Set the default start cell and offsets for the comment. These are * generally fixed in relation to the parent cell. However there are some - * edge cases for cells at the, er, edges. */ + * edge cases for cells at the, well yes, edges. */ if (row == 0) y_offset = 2; else if (row == LXW_ROW_MAX - 3) @@ -2559,20 +2631,16 @@ lxw_worksheet_prepare_vml_objects(lxw_worksheet *self, size_t used = 0; char *vml_data_id_str; - RB_FOREACH(row, lxw_table_rows, self->table) { + RB_FOREACH(row, lxw_table_rows, self->comments) { - if (row->has_comments) { - RB_FOREACH(cell, lxw_table_cells, row->cells) { - if (cell->comment) { - /* Calculate the worksheet position of the comment. */ - _worksheet_position_vml_object(self, cell->comment); + RB_FOREACH(cell, lxw_table_cells, row->cells) { + /* Calculate the worksheet position of the comment. */ + _worksheet_position_vml_object(self, cell->comment); - /* Store comment in a simple list for use by packager. */ - STAILQ_INSERT_TAIL(self->comment_objs, cell->comment, - list_pointers); - comment_count++; - } - } + /* Store comment in a simple list for use by packager. */ + STAILQ_INSERT_TAIL(self->comment_objs, cell->comment, + list_pointers); + comment_count++; } } @@ -5080,18 +5148,9 @@ worksheet_write_comment_opt(lxw_worksheet *self, lxw_row_t row_num, lxw_col_t col_num, char *text, lxw_comment_options *options) { - lxw_row *row; lxw_cell *cell; lxw_error err; lxw_vml_obj *comment; - uint8_t data_changed = LXW_FALSE; - - if (self->optimize) { - LXW_WARN("worksheet_write_comment/opt(): " - "Not supported in 'constant_memory' mode."); - - return LXW_ERROR_FEATURE_NOT_SUPPORTED; - } err = _check_dimensions(self, row_num, col_num, LXW_FALSE, LXW_FALSE); if (err) @@ -5109,40 +5168,24 @@ worksheet_write_comment_opt(lxw_worksheet *self, comment->text = lxw_strdup(text); GOTO_LABEL_ON_MEM_ERROR(comment->text, mem_error); - row = lxw_worksheet_find_row(self, row_num); - if (row) - data_changed = row->data_changed; - - cell = lxw_worksheet_find_cell_in_row(row, col_num); - if (cell) { - free(cell->comment); - } - else { - /* If there isn't an existing cell we use a new blank cell. */ - cell = _new_blank_cell(row_num, col_num, NULL); - _insert_cell(self, row_num, col_num, cell); - } - comment->row = row_num; comment->col = col_num; + cell = _new_comment_cell(row_num, col_num, comment); + GOTO_LABEL_ON_MEM_ERROR(cell, mem_error); + + _insert_comment(self, row_num, col_num, cell); + /* Set user and default parameters for the comment. */ _get_comment_params(comment, options); - cell->comment = comment; - - if (!row) { - row = lxw_worksheet_find_row(self, row_num); - row->data_changed = LXW_FALSE; - } - else { - row->data_changed = data_changed; - } - - row->has_comments = LXW_TRUE; self->has_vml = LXW_TRUE; self->has_comments = LXW_TRUE; + /* Insert a placeholder in the cell RB table in the same position so + * that the worksheet row "spans" calculations are correct. */ + _insert_cell_placeholder(self, row_num, col_num); + return LXW_NO_ERROR; mem_error: diff --git a/test/functional/src/test_comment16.c b/test/functional/src/test_comment16.c new file mode 100644 index 00000000..ed21ca78 --- /dev/null +++ b/test/functional/src/test_comment16.c @@ -0,0 +1,30 @@ +/***************************************************************************** + * Test cases for libxlsxwriter. + * + * Test to compare output against Excel files. + * + * Copyright 2014-2019, John McNamara, jmcnamara@cpan.org + * + */ + +#include "xlsxwriter.h" + +int main() { + + lxw_workbook *workbook = workbook_new("test_comment16.xlsx"); + lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL); + + worksheet_write_string(worksheet, CELL("A1"), "Foo", NULL); + worksheet_write_string(worksheet, CELL("C7"), "Bar", NULL); + worksheet_write_string(worksheet, CELL("G14"), "Baz", NULL); + + worksheet_write_comment(worksheet, CELL("A1"), "Some text"); + worksheet_write_comment(worksheet, CELL("D1"), "Some text"); + worksheet_write_comment(worksheet, CELL("C7"), "Some text"); + worksheet_write_comment(worksheet, CELL("E10"), "Some text"); + worksheet_write_comment(worksheet, CELL("G14"), "Some text"); + + worksheet_set_comments_author(worksheet, "John"); + + return workbook_close(workbook); +} diff --git a/test/functional/src/test_comment56.c b/test/functional/src/test_comment56.c new file mode 100644 index 00000000..a7294f31 --- /dev/null +++ b/test/functional/src/test_comment56.c @@ -0,0 +1,41 @@ +/***************************************************************************** + * Test cases for libxlsxwriter. + * + * Test to compare output against Excel files. + * + * Copyright 2014-2019, John McNamara, jmcnamara@cpan.org + * + */ + +#include "xlsxwriter.h" + +int main() { + + lxw_workbook *workbook = workbook_new("test_comment56.xlsx"); + lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL); + + worksheet_write_number(worksheet, CELL("A1"), 1234, NULL); + worksheet_write_number(worksheet, CELL("C7"), 1234, NULL); + worksheet_write_number(worksheet, CELL("G14"), 1234, NULL); + + worksheet_write_comment(worksheet, CELL("A1"), "Some text"); + worksheet_write_comment(worksheet, CELL("D1"), "Some text"); + worksheet_write_comment(worksheet, CELL("C7"), "Some text"); + worksheet_write_comment(worksheet, CELL("E10"), "Some text"); + worksheet_write_comment(worksheet, CELL("G14"), "Some text"); + + /* Repeat above to check for overwrite leaks. */ + worksheet_write_string(worksheet, CELL("A1"), "Foo", NULL); + worksheet_write_string(worksheet, CELL("C7"), "Bar", NULL); + worksheet_write_string(worksheet, CELL("G14"), "Baz", NULL); + + worksheet_write_comment(worksheet, CELL("A1"), "Some text"); + worksheet_write_comment(worksheet, CELL("D1"), "Some text"); + worksheet_write_comment(worksheet, CELL("C7"), "Some text"); + worksheet_write_comment(worksheet, CELL("E10"), "Some text"); + worksheet_write_comment(worksheet, CELL("G14"), "Some text"); + + worksheet_set_comments_author(worksheet, "John"); + + return workbook_close(workbook); +} diff --git a/test/functional/src/test_optimize13.c b/test/functional/src/test_optimize13.c new file mode 100644 index 00000000..67b1f578 --- /dev/null +++ b/test/functional/src/test_optimize13.c @@ -0,0 +1,25 @@ +/***************************************************************************** + * Test cases for libxlsxwriter. + * + * Test to compare output against Excel files. + * + * Copyright 2014-2019, John McNamara, jmcnamara@cpan.org + * + */ + +#include "xlsxwriter.h" + +int main() { + + lxw_workbook_options options = {.constant_memory = LXW_TRUE}; + + lxw_workbook *workbook = workbook_new_opt("test_optimize13.xlsx", &options); + lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL); + + worksheet_write_string(worksheet, CELL("A1"), "Foo", NULL); + worksheet_write_comment(worksheet, CELL("B2"), "Some text"); + + worksheet_set_comments_author(worksheet, "John"); + + return workbook_close(workbook); +} diff --git a/test/functional/src/test_optimize14.c b/test/functional/src/test_optimize14.c new file mode 100644 index 00000000..3a24eec3 --- /dev/null +++ b/test/functional/src/test_optimize14.c @@ -0,0 +1,32 @@ +/***************************************************************************** + * Test cases for libxlsxwriter. + * + * Test to compare output against Excel files. + * + * Copyright 2014-2019, John McNamara, jmcnamara@cpan.org + * + */ + +#include "xlsxwriter.h" + +int main() { + + lxw_workbook_options options = {.constant_memory = LXW_TRUE}; + + lxw_workbook *workbook = workbook_new_opt("test_optimize14.xlsx", &options); + lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL); + + worksheet_write_string(worksheet, CELL("A1"), "Foo", NULL); + worksheet_write_string(worksheet, CELL("C7"), "Bar", NULL); + worksheet_write_string(worksheet, CELL("G14"), "Baz", NULL); + + worksheet_write_comment(worksheet, CELL("A1"), "Some text"); + worksheet_write_comment(worksheet, CELL("D1"), "Some text"); + worksheet_write_comment(worksheet, CELL("C7"), "Some text"); + worksheet_write_comment(worksheet, CELL("E10"), "Some text"); + worksheet_write_comment(worksheet, CELL("G14"), "Some text"); + + worksheet_set_comments_author(worksheet, "John"); + + return workbook_close(workbook); +} diff --git a/test/functional/test_comment.py b/test/functional/test_comment.py index b7401c74..e5cc17a3 100644 --- a/test/functional/test_comment.py +++ b/test/functional/test_comment.py @@ -59,3 +59,9 @@ class TestCompareXLSXFiles(base_test_class.XLSXBaseTest): def test_comment15(self): self.run_exe_test('test_comment15') + def test_comment16(self): + self.run_exe_test('test_comment16') + + # Memory leak test. + def test_comment56(self): + self.run_exe_test('test_comment56', 'comment16.xlsx') diff --git a/test/functional/test_optimize.py b/test/functional/test_optimize.py index 7fda5724..712241c5 100644 --- a/test/functional/test_optimize.py +++ b/test/functional/test_optimize.py @@ -33,6 +33,12 @@ class TestCompareXLSXFiles(base_test_class.XLSXBaseTest): # Skip some of the XlsxWriter tests until the required functionality is ported. + def test_optimize13(self): + self.run_exe_test('test_optimize13') + + def test_optimize14(self): + self.run_exe_test('test_optimize14') + def test_optimize21(self): self.run_exe_test('test_optimize21') diff --git a/test/functional/xlsx_files/comment16.xlsx b/test/functional/xlsx_files/comment16.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7e0b5b6387dfc7962df847c509eb25e341b27b7d GIT binary patch literal 9085 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 YoL5suN22yeO9CIDjRZa+{g12v0h5)EKmY&$ literal 0 HcmV?d00001 diff --git a/test/functional/xlsx_files/optimize13.xlsx b/test/functional/xlsx_files/optimize13.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..fa4bf9c4f6e322e60f31469e41415931cf0a44a0 GIT binary patch literal 6375 zcmZ`-1yq#nx*b62P^6^0yF-v6q@<)f285xz8>DdvK}qS5k}he)A7FKhL+{cW*T%lzRjK0013v(Evnwj#IRe0D#SV0Kg-}HwMy< z4sMnXZYG-EPL{4loL=^J7%nr~*HziO=`-+q#h7Ps zHZcp^Y) z_ase7`^)0zc;@_e@B>6@zvwfO4YPJOgbMX0?H{Io%E z2YBJ4Y)_usL`y&9(8Ut*M*~QE!cXP^64pP}+4^777J8Zuyw=!ZzX-~RjXC1EbcWIE zWFJbVJV*-felb#A0ND>byJ6tp#)>lN=Gnzwp3F0kq$ZdG45a1q?b0w^nC7(LR&F#1 zig(08pj_~`4V3jFSZuW|K}&*kj*y+A8;Z5%r6g&ex#EMVt5!JW=-jU_XJl%Bwc+r+ z!t54;ekEi801L62xLDe`a&g|imnU|rbaP`%!u@2|F4G+3(Q^t~sPh*X`6W8&9mwk9 z@;-;|!K30OvE#sKubqEvT0wV)pTup)y*MGqW-lcrxDOqP80moCfSvJJu}~xE(0HBl zvC}tRUO%T(EE#Bp=n=~IqxvMAWxLmlE5BoVO)#hm1xvAve%Xrj=y-knp#tu9F%Yy7 zR#HDAIor%oe9bd1VCP`qxRtPLmP>7=e>KQ#@th(m*!v4SBV1=AhgA)>X{0^fawT4r zB@fHVuFtT3s8OP_0@ZJKC!aII*c^>jl4|(XM4t94gw9hB19ZhZ&Ljl$1N_a%x(r#~ zB!UTc1S6z~)yt0SHz}TuF1F^5j<&a>`J14G(P(5I>ljIq z=?KkgcHF`Xd7(D3X>@C2$IB#zYBLt+jNcn z^;m6@XR>hofshuGjS#BOtKFUEn&YkAQ%M!dj1Chjym@?ukhHJy){S)}qXqr=g!@%P z$CfZ2{D6sm3lBHd@xOxl3&^7@}hwkE;ESTI%y}Ka1rn~E=uUj*j{b2EpQRwV@Wp>s* zu&)ku3RqQ^-?c09VAcMzi%kErIEZEMDm0!_7FQ#;u*VxcrZy5zt)skIY6h3ngG@fKG4+Y{Il# zw)6D-Y(8i?zdM`azNV90xb*3llQr@1&$O2)Y}_o$l`mWzVYZ87VKk1dBzaa>d?4Pc!-3UZq+!c0hVO+ww_eArr>UGCNm@TflliRPX@Fy%(29roAtTjk zB-ak|{(P0nc|rwRnQ` zdM3%J?(t+@z9T>*+Ke7GJ!be$KV@3?*y{XUSV>;Nh|Q)ne$9*-{HS)uy2wegXVyf; z%DVNHP#n<)zs!g&=p@}dhgFoiYgy&VLnS{-vBktylq?>Ng{L3V0ib}ZeVb1HkMJI} z9^z)vO_$^kU8c&PsJ|B~)17lFDbPBKsF9MwJLuP%%`r%PWkJT{<-b07{>UM!Xkbz@ zIjcQ8&tIsd5vmR2dznRlV>>%^<5)(rE&l6uR;9g)3 z&F-n4Hw=yYj$fO&rOj|!bB@GQjYmlXr3-v74~&0GFFoPVGPn|0(7X|L|LEVejrVsy zn*}MXu@T~Of#@gM?|xdin0bO7tX#P~?CoTJ-}7)H?&Dr`I?QonUlRgb7~|~uISOY{ z!*N^fQRjL`+5A?#VuuQCzOOW;Cm~&a%m>E zl2sZdvYqox(4){Azm3jzua669&qkpo=2s9*alzTc=6etIXTWR_CrLpyO16sR&alVp z7M{tLin(Q8BRcL)&8!trvx^>3QK~v&ASItqR#iXd2x*Y<{i8xQ<26scdU=zPVmZ0t zM@30rwdXm0+&g(RrUEVYo<;WkYI}m*i}pm{W)pi`V*9a-U57`j<}=^t=B6gju`T?| ztq$DLrCzG)qMc#`R@d;lh$n1Q8OmDkD{%SYi$HGc4y+~&Q@moTj31u8T;+?o{WX{A z<_q6gqF_;%>F>M*FbxjQUw3Jz8^xHns&veadlkqfJjG^w7gvP}$rVrpkIqc#&IP^7 z`eL^==KX|BFJ~;6R^%`!-Y@!jN^#GtmNdMQ<>X-1k)Z`aZ9(7dI*j3*X;vv>xu=br zojVw^q_H@$Ok9RR1-xU1ycR4R(R#0WyQ3Z{q4SDy&sBM6%jx|9gm@eudLi-3I_P z5EFEJmPBmXZq}CemcQTs(ac>vWyr)clC~qZXKkUYpK)gh^=bzgVO?D|LTb(#UmyAQ ze+3Oc_ZTP-0Tn1}fhUy;7JZ6mX@kFDT?@>;sMB2GI{qn_c<{2?!mN}Ch#O^f^=XMJ zps&g0CvXTx^Dyx+-(KXc4H0eBC`wwG{mw{sV3j3Ji4(qQ@1ybhLK%IX22R7ybXHm( zpk!9R@_apQ!MO5Vii91=JF192*q4seyv&qEO6?|#5F(!a4Dg? z&2F|30Non$vdLa?N%d^ln4D)dUR~1da&!}=aLfyFWZ&PzZ_~1Ep8JWpXtdmFc`Y`# z;7}8CN_4h?-xlbdx21g`!fej}m@B8oYlMz3B38J?A*=zgn8wh7Y-u~A1!J<&s^~eB zAod5Wb@oIv$}4N|c1bKZ3sYi7^C5X9neM7otwRgSpA#jvWtZ3(P!*A|nj?Atyz0{# zeV>cC$e;RoTclZz_QvR8@X*QM(F>yw?g zFSEykx!M8_`)ZiD+Rm>=sVK>o=aBK-&w_eoat+qdcaKQy_|U1Podu(TVc=KDazXq6 zU)B!*M`a$qZ=(6zpIVSfgD|}8UD|VZfb@*fq(MTtjmY!s8SahTX|8lh1?hYWQZ-II zW6qZ(RB0BTs$um)3N%S2QIC-HL}~roq_Gsc8gZYC3*3zQpdrd{s*{5nA*F_g*rlglWxt;iwY4jw4-tx7DAn zE6d1$SJ+i4Y{(kvCQl;Q8YB{-bayoUF4OxFUJW*s#gy%1TvHxudOiPb)gf&4 zOEI4^EyZO5xpcI7ZC|oFhH-hk9gcIyz@XDoflOw3863vV%Ovk%MLSqwwAId|tYX_6 zDd53veV|~^3t?>+@O4jQKd$t3*oZjd>43#eP2`Tb+(>i$NXlWz zdw4Jzo!L|UsgEf-GL%_ZN?P#YCCRZz%n!`sFso<&^$*l?98$d|2f0d@*VxjkMY)ye z3{Ub?h%S+{Oh(=Nb<%uJFc+3!Z+b=2R$AK&EmE;Swsie?n%t=9vDM`E28P9v(!`2% z+o$)jahup-SU^1sR+^Mn#}PM%h?q95edaVnk`jMXT42sZiTs1=B1JKi?xB*M(dox| z)uTg`$>0Mq`iL-`>TUa%CkD)E?n&bhMfNOUb{g(D-8a9y4xM$LOB6xSuz-7dFCXgI zUd-m1uI_3IujbV|)Hm*xUl%KX+YhehvYyvGv*TI9J(G~38fH?w7v)u@m9pgLiZ0r~ zzsjrqYmcN39cU~2Y1H-XOU8okw;vNdtxYxJp_Q!@M)q89i_0l24kyQ{RQ$Bj5Dko;EoTOZ02$!CLE>x*%sen*GO8 z%N)>)DrciF-uj|jINk)n zOe4HKrrDD?%(#NhEZC^8BOucP%q#@t!#p2@v9Rn~-VQU3YHr8IYT#?p0I_2W@;zm$ z=zQ@h9z|>)^Z2sX4WlqtoA7g2q;23F(V;Zo*VxSr{^ZF~+|6)c^0f-i1Z}Zy7mql! zxSw_P33{yISgJXQLX4uH*J29^D7D2odor>8lyBy{V+uR|b#%GJ5@>m4X@T(XnKW)L zn((}}Xo#chtxdCV1gW_=I=OP0IXT_BkxWE>vUBT35c!F?`wN<5O_68CLAv`g1w^`J zR^z(D_3MvwQ!4uuCgYLWs+;*--Tis%8)xeTcc^m@DlB@~tsV>SvaQxL_|DE}{sKKt zbV;sW1P+`8{ZPz`LZl3|k2}~IgxU3^35YWC9I~~9l|XM9>mcR1n!OyS?)LoiUe}8u zbP}USO1NUkIzV^IUP%vmL$~8;*x*hU-g}|T_{020+)mPR=Ii_`N4Mkewu26yq-Dz! zIr2bT7<>$7yb+?RR5dl&#d{yBuXnGropgm#v%Myn_hG?z>#<-)Dn5QHu&rxKD{B~K zkws?oGZAyNxg3&O9C`J&v$Jp;V|^M;QF;Mk~y3ltr*~>Nd2esHWcmtgiOx({VaWxLjhtf z#ri)q-(CeDjejRye+F^J$}d_!Ofv-#s9?`03jNd?%`PP?M&w*GK^nN zLLbj^G03w=Pci>-PDF5|=k}esB2I(-^SJ#-%c}@`0CCvR$B>SDSlIC3lFZ2OkpzBH zvnJDVfKDE_Y)Lf85qqDNBcSMNI9$ESiS;ommQXp9vkUxFMy&smmA{BS=!QJ7+<2)^ zOQYnpXeB63(P@z4qp6|BHz+M-1xTW7C2AV~^TdOnWR88Zxc=0MBteubS#L%U&kKxl z3#sobC}bsi8@#E_nD9GkrShH(ME$TD-x9^tL38?($@*4|k&p>c{(E?fAQpfi8IT`* zd-orr+`9mGW5?UF-vAMa5j-3|E<04G8V|Chgi+n(>D+}%(9pwuBA*1u8y}1dEx2U0!Lub^tjWvb)>amL$ULJ2~-XPcFmzW>~pg)U}EHWx?;BVEwvNDoR7_({EKcR#s`?%D+%e0-_VVG^3nBhFNZB-T6MW0I0Bg{O7 z8>`&omUG4qu%LH+Q7M$SJ196nR@~l+?x0oZRZaWN?&a>|6%C-DI5t|$OOJQ$c_L4# zY(-L}7<3pxWd0K^%r=U98C%9aby^V`tNa;)xd_YO$5u9s20aAJKYZ2vP>X z?cljz#XAa|ue9|8_MJe;Kk7r)9eg|k5VJZ`XX~?|&37~DkJj7*{_;zWj5y#rcZAdH zX6{QQ;U@%lX?(5Bi`n%(y<*_q#0oR$;@ZYu9Lq5ceM~qG=ugS!*`{IoWt`Q7Teeom zC)yqr1LHuvtD~wF#A2&%@>}4egT!ooyrNuLTu6}im?_vBzi2^F4b5DSo|3EmCBuHk zv*`^a{YoeR02Xq6I=3u{hFNyC^>EgteKzPZnoTu0;U}WVrJWf=M?3ypw}fqd5sBK2(m1BT1{RSK-h+J&`PvS<0z2ZdVxfi5p>sRr zVt-h(xO`5hSlHhZqerCBhvpG?nhC8HResMFO*o(m152?Cjc$axwnra+Dn+>b>i1g< zDy;n~G2O^eaLG0N)YjervJtm!lKt3H|6+jI{5fTq|LajiYOwBF7ONV3-B4$u=|c2l zx&k~avo_W0fo7q~5=_4hN-<-2e|;!YNviI;fgE-=w%foNP=X5SyFU{7q2YP&f(~b_haDw%?nH2b0#c7go8beRxNa z=>Xkwdf418t*(x_kuB7Hm(b__*}kOC)-t?^I(+u^$E-dlwUy#Bp$V%=M5`Ea=8MRp z=LkwLX_$$jijKi%R4%1`Q#-39g^iVLCqbN@QsDPNbSJRJxd7=iEKo|0W}l)S5v7`IIsu$-w(f#=p`K zr4B>32!8Vc(fB-=%9>99Y2jxjEh#@4@DSrXuMVYfCFSsy=$00X!*Vsx^4zzt9t}c# z;oe!$;U5(7&#;=Cd1(`erAzlqP8LjIsos?rT-DWi)!U`@7PvQ`Xc#!1p$uf* z0ekAg#w9C?b33;M@t5t+JIVFW3;bAiE&^kzWN|gK^Lxbuwx?%QK1o>YVP`K9?0@0* zCqg7Hz^!wH)7aAT!Nfw+6FQ~{pI{VFZ`x?*p@8KlTgp#WccJMw*CqlI{De zt;d3cRY2UeZyYDzeat`}7$;N?QrCo=DaTB#}z!nF?M)WZcE{DPww42Dn;?n`q&X1 z5m0upS8=D~S%{=CX2D`GcF0cFcE2ZfnCvmSjm8?34FX(vP9Kx4 zRn1Mr*OsSN>UOCLYan0RA^E!@eedD{@{qjLp#lInzqxUCd2I`Fz9A+r0Tw=cvtgWJ zW_kB)2V*A~dlLHxOAw%p_}dm4fDAH2;s&D9%2fMfjs>vIixghIwfm& zkC9W~hE<=Ok7!koOX*6Y#)RZ}L2gX$@(j}8*u)2~!4 zty%9tTF0dH z>a_awEO)MqhQB(9$0D8n%4T}-3Q|nEDS9299_f16#5(*XY2VW$-$FW6sSF2vBG)yj z)iu7A$WXrvnQY*cHoq-Dx?%lA7_qU!V zkqH%q$l-F0)RX+Tp5{&_ZeV*$XAW08TbbYYTA#he_HInXXr$&&nY_O<#k)XvbohC75ty?Y_(Q36s~H2DlZLHn zxrvKX?5H33u%*gtrK8Qg+~|XTOonFnE;W%yLCxwIfsB=TQHe-9uAb^^V1o|YLyN?O zZS5qMu*wy|3~ktywqwoDm^OE-r#zol%B1Cn*ogHy^l;#X#`Dnr2(LI$!r@w^NA;+P zqkfoj|J-M)jva;S-l5Fb(Ok+eyrixP5n)?Pr7RLNq_moCd1DOsBXJ^X-%%46nzD<`&~K4*^mOyp z95fJNc%UEW+u9aZ*rGle8glPV6yFE5Zm)E|lfpekJX+tM%A9OwI8-ubiNontH5IjJ zSd<%jBRL1|Lfq0hsImboxHN)9$w2i8EH^$X&5m?`)%nOd9i4H5%^J+3(h*iE64mSd zp}+}wLNKomXX>oOo%{hA&;IzrYGygXfkP%0AWffbb12mV0Xkm8p2-q@+@<~=;S0r) z9-rd*g6~SxXTrv*fs$T?poPyj2Sdx&(?hI%9CBEP4CN_ zR(}qLdkz13UaCDbjFwie84S-TFWaAaAElnCaI)=H6I#fn@?Gx?)K0r-dxZXXGl@?k zXe#aj0Gh~7x;axKw{90JkR9mv`#-aDTTeM=M2@uefK9G7aQQRt6p>zaKO?-e)0$t+ zF?I5xXWyjRm*=kiB>`r6irU~YrM!8Mf@xa+QLM|SGa5BoOB{zs^6`5XmF6Zz#FDsS zmKV(n)IPlpPDhf1aGD44`?+?4@2rVw!-h~(g6y`wX8KluXbK$&jC&pq*XGOU>(;Tq z?D)V+%Oxq1-lsfUOPe>WJd-45YxX+qBfY;T9lL33ZU}Rp+Q?q%q-M7ricBjuNP8(#>XnWDjn9k!gr$8u_rsK5|D(x|7EB=-E zT^b!xwc0T8q!ws*`a`ao0G*tz^^EvZdW@cVq4JJ zM-Xr>_F@xgGUo%^7<9MJTyjcwt6LkJWi?t}c+m-Q5u$|T1VDhhI|QxTHjOh!nDd5< zEuc%`nK}EafD_`=HG)>(*Et(HdxFfSypK4ts@%WQ@q|PQG}#B$0p?Q}+EG9@Q`&GQ zYwgnRQ!x^6z)DAVIHQ8HCU>XUVxs^RW;o9s_XOi@m5NnZUdeOf_}0uqYXjJDXOBQFYZFV~CRcgylI(bQMAkDH?t)R?@^E=hcS&slzLZMbnz`+|CuD6h{| zGcJ#}-dSW0`*XDV?DtkNakQRY3{g{&FV3LgK~Md9WU>ubFt!g!ZFw*rOFQy~O9p}8 zpve320z6qi0U*j;Jl}(&;QHoxPD-6(?xq@4%m0ifp)d@@PfPUlgj9w+gq{WybaMJ0A9YGpkyD#J7#&5n z{C=bF!wXqP2E6>vGJ%(@p)Lxf@-2P};OvGc`9x5N@ei4vazrKgr7WgwFXM{xU_i3OGVr_98+xrI`7V>1$N{SJ1PHrXzS4-M~Qp1fl zE@c&)o=7SFD|n>kuAMt?vf`3CmeCtebaDi4I=$yzr%UODOSs!YZ)?-1q>1g?2sE25 zXUDG`MuPM)%x37;1AWTLbr$y((Y+@JNd&)nF{;wl1G}{yvf9s{61?=5(ssfBPWV}% zAxJH$SUOCD{X4yJ$x0eE6ANhu;&m0zv7t#648kHfD?qE933(6GM5;Ax;Q0QqGX3@b1iad$;9j9yMIG=TZQc=W{pFSS! z%(XlF#JgJ4?KN!dtS?ma7EMm3xX`=yle5*vh0)H#|Aix7FXC( zDupbq`w-1_UWd09AnNcz!Q4}Q2SAJ9!e$_Qt zxHUBKD5r90aO^F3PnbR=2&Zz>&f?gBIR%<9{6KKW9B!)##p$}bcHei@eJ=Jf1|3Us zN6+GcuFbFM9OLC}ErI2nTKn4ios!D}s;@U)?i55$Tkl-pN^)^z4-ofq`Re|N;I&nWyH{q<6S`srTPBYFtw_e zJGD8YT@u`x2%KjW^~@Kw*XTT9msW&xPc3ITaE)T%CdMQ6 zm^v>+Pw?IGXm*o87n2P%o7`n-qr+n1nJkCfUXJ{2qxpj$$Hgfl_vm$O-3)fsxJFI= zbih@VOPZ|qD=bPH2k%@(m5Ij?nT*X42?ntnl=V}M`g~(tZz^6AsvaX5Cn$-@b zVs@KnE7yDT1G;~UR2{1Ja1bd@9a1Q|e-r7qnf@cyZz~-Ovqt5@Mn*lDIo>77Kt<*~ z^M{yINh_#9C9mOos4(S(wisz<^T!eFjR`xl^19QuLq0l8U%9)DGske4arqiqu+gGJ zVkVw4vk+2z;VSpX!m@38_l0RlYcn!ZlR%qB5<4<4*G;B^&Xa)hVaVnomnUn@7gWZ| zSG=61DQh?fbf}H@G`G|Io86uibTRC`nyf%D!J4evL__w?@1B&hm1 zO*Sz;MK(C6A|spPJX1d)NkD?j@Di~Fv&E%_IikPGFlzib;&x-?kO$lwGiMGlQ*(ki zICGddINW%aG-S%Mb>mr(DT^pngXU06P_Dr5#jZ>q@e6Xx;THn6tB%;E!Q%5PS2)Yn>~tmdRsX!*?;W!Q!zaZnMKe( zY6mh10QIB^iBogzGqnYj%-%89#FS)f^*lL&+VRf1U(N^6i47en;R>VZNCuoX3lSNvMqoCe; zdK!!a$XYaWj@3kHeZbPg4yIR$^1A!%7onqBABxyG8_@>IlBuEyXZii*#&tHIT}sto zNiBzVYV1#9V>UbL`Mog7eRD9Pw?yp1p*gf#8)!Yy7#zop`t0pXm|VjXE%~7#N#C=f z5Ons4Gob2%!+LM};wy-r56a&nsgph9pht?Nh8!yo{#7J1h!g0yN?&d)cMx)(aJ{tfEAMjq-=D~YMn;=Zd?w?? zlXZGeUs^mNl0WP=>A2S=+|EFQ_d)txAw=Nyr(Lzx(Q0fRmrz(Ni zcp2q(9f^FF71~qu_nKzB9GEeQ^QP_t-ZMV0mtU>&@X`CKZV(^8610{GR2ujc|Lq(x z*B5d1P>nix_PGekaRZ0A6^P~M!{U`OfU3%M6|0zXBM(@ZU{%;2RHqdQ5^#@1`^B