From 55ac5af43bd15c1867688f5fd9c4ac24502b7ff1 Mon Sep 17 00:00:00 2001 From: chrassig Date: Fri, 14 Mar 2014 16:15:09 +0100 Subject: [PATCH] More sophisticated calculation of cell widths Let me start by saying we really love the ABAP2XLSX project. Compared to exporting CSV files and importing them to Excel, it makes transferring data from SAP systems to Excel and vice versa so much easier. What we did not like so far was the optimization of column width, especially because of the way this is realized in the XLSX file format (why did they not just add an attribute "optimize column width = true"?). Anyway, ABAP2XLSX being an open source project, we decided to try to bring some progress to this feature. We ended up rewriting the method CALCULATE_CELL_WIDTH of the class ZCL_EXCEL_WORKSHEET. We added the following features: a) Before, the calculation of the cell width in ABAP2XLSX did not take into account the fact that Excel uses a proportional font by default. Thus, columns with many "i"s ended up too wide, while columns containing many "W"s ended up not wide enough. We now use the function module LOAD_FONT, contained in SAP package BC. Thus, it should be available in pretty much every SAP system that runs ABAP. LOAD_FONT returns an internal table containing the width of every letter known in a font. You can upload Excel's default font Calibri using transaction SM73. Make sure to upload all four files (standard, bold, italic, bold and italic) and to use the description "Calibri", i.e. exactly the name Excel displays for the font. If you want to use other fonts in your files created by ABAP2XLSX, you can of course upload other fonts as well. We calculate the sum of the widths of all characters in a cell, and add some cell padding, similar to the way Excel does it. If the font is not available in your SAP system, we revert back to a logic similar to the old one. As calling LOAD_FONT for every cell took several seconds when we tested with larger tables, we store the values read from the function in a hash table. Thus, we only have to call the function once for every combination of font name and font attributes used in your Excel file. b) XLSX stores dates in a cryptic format. For dates, the current implementation calculates the width of the cryptic values, thus, the columns end up too narrow. Fortunately, ABAP2XLSX already contains a method to transform the cryptic value into an ABAP date. We then use ABAP's WRITE statement to create a date in the format the user is preferring, and calculate the width of this formatted date. c) ABAP2XLSX currently does not consider auto filters. Thus, a column may end up too narrow, not showing the complete header text. We fixed this by adding a fixed value to the cell width if an auto filter is activated. We use a fixed value because the size of the auto filter button in Excel does not depend on the font size you use. d) ABAP2XLSX only estimates the width of the characters. Excel adds a small fixed amount to the width of the characters to create some horizontal cell padding. We added cell padding as well, as this brought the cell width even closer to the values calculated by Excel. e) ABAP2XLSX already uses ABAP's data type FLOAT to store the width of a cell. However, CALCULATE_CELL_WIDTH used data type I (integer) to calculate the cell width before. Changing the parameter type for the parameter EP_WIDTH from I to FLOAT, and adjusting the variable used by the calling method CALCULATE_COLUMN_WIDTHS in the same way, made the calculation even more precise. --- ZA2X/CLAS/ZCL_EXCEL_WORKSHEET.slnk | 284 +++++++++++++++++++++++++++-- 1 file changed, 265 insertions(+), 19 deletions(-) diff --git a/ZA2X/CLAS/ZCL_EXCEL_WORKSHEET.slnk b/ZA2X/CLAS/ZCL_EXCEL_WORKSHEET.slnk index e1b08c1..104915d 100644 --- a/ZA2X/CLAS/ZCL_EXCEL_WORKSHEET.slnk +++ b/ZA2X/CLAS/ZCL_EXCEL_WORKSHEET.slnk @@ -1,5 +1,26 @@ + + + + @@ -214,6 +235,7 @@ ENDCLASS. "lcl_gui_alv_grid DEFINITION + @@ -2860,31 +2882,255 @@ ENDCLASS. "lcl_gui_alv_grid DEFINITION - + - method CALCULATE_CELL_WIDTH. - DATA: cell_value TYPE zexcel_cell_value, - guid TYPE zexcel_cell_style, - stylemapping TYPE zexcel_s_stylemapping. + *--------------------------------------------------------------------* +* issue #293 - Roberto Bianco +* - Christian Assig 2014-03-14 +* +* changes: - Calculate widths using SAPscript font metrics +* (transaction SE73) +* - Calculate the width of dates +* - Add additional width for auto filter buttons +* - Add cell padding to simulate Excel behavior +*--------------------------------------------------------------------* +METHOD calculate_cell_width. - me->get_cell( EXPORTING ip_column = ip_column " Cell Column - ip_row = ip_row " Cell Row - IMPORTING ep_value = cell_value - ep_guid = guid )." Cell Value ). + CONSTANTS: + lc_default_font_name TYPE zexcel_style_font_name VALUE 'Calibri', "#EC NOTEXT + lc_default_font_height TYPE tdfontsize VALUE '110', + lc_excel_cell_padding TYPE float VALUE '0.75'. + DATA: ld_cell_value TYPE zexcel_cell_value, + ld_current_character TYPE c LENGTH 1, + ld_style_guid TYPE zexcel_cell_style, + ls_stylemapping TYPE zexcel_s_stylemapping, + lo_table_object TYPE REF TO object, + lo_table TYPE REF TO zcl_excel_table, + ld_table_top_left_column TYPE zexcel_cell_column, + ld_table_bottom_right_column TYPE zexcel_cell_column, + ld_flag_contains_auto_filter TYPE abap_bool VALUE abap_false, + ld_flag_bold TYPE abap_bool VALUE abap_false, + ld_flag_italic TYPE abap_bool VALUE abap_false, + ld_date TYPE d, + ld_date_char TYPE c LENGTH 50, + ld_font_height TYPE tdfontsize VALUE lc_default_font_height, + lt_itcfc TYPE STANDARD TABLE OF itcfc, + ld_offset TYPE i, + ld_uccp TYPE i, + ls_font_metric TYPE mty_s_font_metric, + ld_width_from_font_metrics TYPE i, + ld_font_family TYPE itcfh-tdfamily, + ld_font_name TYPE zexcel_style_font_name VALUE lc_default_font_name, + lt_font_families LIKE STANDARD TABLE OF ld_font_family, + ls_font_cache TYPE mty_s_font_cache. - ep_width = STRLEN( cell_value ). - TRY. - stylemapping = me->excel->get_style_to_guid( guid ). - CATCH zcx_excel. - EXIT. " Do nothing if no style was found - ENDTRY. + FIELD-SYMBOLS: <ls_font_cache> TYPE mty_s_font_cache, + <ls_font_metric> TYPE mty_s_font_metric, + <ls_itcfc> TYPE itcfc. - IF stylemapping-complete_stylex-font-size = 'X'. - ep_width = ep_width * stylemapping-complete_style-font-size / 11. + " Determine cell content and cell style + me->get_cell( EXPORTING ip_column = ip_column + ip_row = ip_row + IMPORTING ep_value = ld_cell_value + ep_guid = ld_style_guid ). + + " ABAP2XLSX uses tables to define areas containing headers and + " auto-filters. Find out if the current cell is in the header + " of one of these tables. + LOOP AT me->tables->collection INTO lo_table_object. + " Downcast: OBJECT -> ZCL_EXCEL_TABLE + lo_table ?= lo_table_object. + + " Convert column letters to corresponding integer values + ld_table_top_left_column = + zcl_excel_common=>convert_column2int( + lo_table->settings-top_left_column ). + + ld_table_bottom_right_column = + zcl_excel_common=>convert_column2int( + lo_table->settings-bottom_right_column ). + + " Is the current cell part of the table header? + IF ip_column BETWEEN ld_table_top_left_column AND + ld_table_bottom_right_column AND + ip_row EQ lo_table->settings-top_left_row. + " Current cell is part of the table header + " -> Assume that an auto filter is present and that the font is + " bold + ld_flag_contains_auto_filter = abap_true. + ld_flag_bold = abap_true. + ENDIF. + ENDLOOP. + + " If a style GUID is present, read style attributes + IF ld_style_guid IS NOT INITIAL. + TRY. + " Read style attributes + ls_stylemapping = me->excel->get_style_to_guid( ld_style_guid ). + + " If the current cell contains the default date format, + " convert the cell value to a date and calculate its length + IF ls_stylemapping-complete_style-number_format-format_code = + zcl_excel_style_number_format=>c_format_date_std. + + " Convert excel date to ABAP date + ld_date = + zcl_excel_common=>excel_string_to_date( ld_cell_value ). + + " Format ABAP date using user's formatting settings + WRITE ld_date TO ld_date_char. + + " Remember the formatted date to calculate the cell size + ld_cell_value = ld_date_char. + + ENDIF. + + " Read the font size and convert it to the font height + " used by SAPscript (multiplication by 10) + IF ls_stylemapping-complete_stylex-font-size = abap_true. + ld_font_height = ls_stylemapping-complete_style-font-size * 10. + ENDIF. + + " If set, remember the font name + IF ls_stylemapping-complete_stylex-font-name = abap_true. + ld_font_name = ls_stylemapping-complete_style-font-name. + ENDIF. + + " If set, remember whether font is bold and italic. + IF ls_stylemapping-complete_stylex-font-bold = abap_true. + ld_flag_bold = ls_stylemapping-complete_style-font-bold. + ENDIF. + + IF ls_stylemapping-complete_stylex-font-italic = abap_true. + ld_flag_italic = ls_stylemapping-complete_style-font-italic. + ENDIF. + + CATCH zcx_excel ##NO_HANDLER. + " Style GUID is present, but style was not found + " Continue with default values + + ENDTRY. ENDIF. - endmethod. + " Check if the same font (font name and font attributes) was already + " used before + READ TABLE mth_font_cache + WITH TABLE KEY + font_name = ld_font_name + font_height = ld_font_height + flag_bold = ld_flag_bold + flag_italic = ld_flag_italic + ASSIGNING <ls_font_cache>. + + IF sy-subrc <> 0. + " Font is used for the first time + " Add the font to our local font cache + ls_font_cache-font_name = ld_font_name. + ls_font_cache-font_height = ld_font_height. + ls_font_cache-flag_bold = ld_flag_bold. + ls_font_cache-flag_italic = ld_flag_italic. + INSERT ls_font_cache INTO TABLE mth_font_cache + ASSIGNING <ls_font_cache>. + + " Determine the SAPscript font family name from the Excel + " font name + SELECT tdfamily + FROM tfo01 + INTO TABLE lt_font_families + UP TO 1 ROWS + WHERE tdtext = ld_font_name. + + " Check if a matching font family was found + " Fonts can be uploaded from TTF files using transaction SE73 + IF lines( lt_font_families ) > 0. + READ TABLE lt_font_families INDEX 1 INTO ld_font_family. + + " Load font metrics (returns a table with the size of each letter + " in the font) + CALL FUNCTION 'LOAD_FONT' + EXPORTING + family = ld_font_family + height = ld_font_height + printer = 'SWIN' + bold = ld_flag_bold + italic = ld_flag_italic + TABLES + metric = lt_itcfc + EXCEPTIONS + font_family = 1 + codepage = 2 + device_type = 3 + OTHERS = 4. + IF sy-subrc <> 0. + CLEAR lt_itcfc. + ENDIF. + + " For faster access, convert each character number to the actual + " character, and store the characters and their sizes in a hash + " table + LOOP AT lt_itcfc ASSIGNING <ls_itcfc>. + ld_uccp = <ls_itcfc>-cpcharno. + ls_font_metric-char = + cl_abap_conv_in_ce=>uccpi( ld_uccp ). + ls_font_metric-char_width = <ls_itcfc>-tdcwidths. + INSERT ls_font_metric + INTO TABLE <ls_font_cache>-th_font_metrics. + ENDLOOP. + + ENDIF. + ENDIF. + + " Calculate the cell width + " If available, use font metrics + IF lines( <ls_font_cache>-th_font_metrics ) = 0. + " Font metrics are not available + " -> Calculate the cell width using only the font size + ep_width = + strlen( ld_cell_value ) * ld_font_height / lc_default_font_height + + lc_excel_cell_padding. + + ELSE. + " Font metrics are available + + " Calculate the size of the text by adding the sizes of each + " letter + DO strlen( ld_cell_value ) TIMES. + " Subtract 1, because the first character is at offset 0 + ld_offset = sy-index - 1. + + " Read the current character from the cell value + ld_current_character = ld_cell_value+ld_offset(1). + + " Look up the size of the current letter + READ TABLE <ls_font_cache>-th_font_metrics + WITH TABLE KEY char = ld_current_character + ASSIGNING <ls_font_metric>. + IF sy-subrc = 0. + " The size of the letter is known + " -> Add the actual size of the letter + ADD <ls_font_metric>-char_width TO ld_width_from_font_metrics. + ELSE. + " The size of the letter is unknown + " -> Add the font height as the default letter size + ADD ld_font_height TO ld_width_from_font_metrics. + ENDIF. + ENDDO. + + " Add cell padding (Excel makes columns a bit wider than the space + " that is needed for the text itself) and convert unit + " (division by 100) + ep_width = ld_width_from_font_metrics / 100 + lc_excel_cell_padding. + ENDIF. + + " If the current cell contains an auto filter, make it a bit wider. + " The size used by the auto filter button does not depend on the font + " size. + IF ld_flag_contains_auto_filter = abap_true. + ADD 2 TO ep_width. + ENDIF. + +ENDMETHOD. @@ -2906,7 +3152,7 @@ ENDCLASS. "lcl_gui_alv_grid DEFINITION DATA: cell_style TYPE REF TO zcl_excel_style. DATA: count TYPE int4. DATA: highest_row TYPE int4. - DATA: width TYPE i. + DATA: width TYPE float. FIELD-SYMBOLS: <column_dimension> LIKE LINE OF column_dimensions. FIELD-SYMBOLS: <auto_size> LIKE LINE OF auto_sizes.