## Copyright (C) 2014-2020 Philip Nienhuis ## ## This program is free software; you can redistribute it and/or modify it ## under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 3 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program. If not, see . ## -*- texinfo -*- ## @deftypefn {Function File} {@var{ods}, @var{status} =} __OCT_oct2ods__ (@var{obj}, @var{ods}, @var{wsh}, @var{crange}, @var{spsh_opts}) ## Internal function for writing to ODS sheet ## ## @seealso{} ## @end deftypefn ## Author: Philip Nienhuis ## Created: 2014-01-18 function [ ods, status ] = __OCT_oct2ods__ (obj, ods, wsh, crange, spsh_opts=0, obj_dims) ## Find out if we write to existing or new sheet new_sh = 0; if (isnumeric (wsh)) if (wsh < 1) error ("oct2xls/ods: sheet number (%d) should be > 0", wsh); elseif (ods.changed == 3) ## New sheet name in new file. Ignore sheet# other than to name sheet ods.sheets.sh_names(1) = sprintf ("Sheet%d" , wsh); wsh = 1; elseif (wsh > numel (ods.sheets.sh_names)) ## New sheet ods.sheets.sh_names(wsh) = sprintf ("Sheet%d", wsh); new_sh = 1; wsh = numel (ods.sheets.sh_names) + 1; endif elseif (ischar (wsh)) idx = find (strcmp (wsh, ods.sheets.sh_names)); if (isempty (idx)) if (ods.changed == 3) ## New sheet name in new file ods.sheets.sh_names(1) = wsh; idx = 1; else ## New sheet ods.sheets.sh_names(end+1) = wsh; new_sh = 1; idx = numel (ods.sheets.sh_names); endif endif wsh = idx; endif ## Check if we made a new file from template if (ods.changed == 3) ods.changed = 2; endif if (new_sh) wsh = numel (ods.sheets.sh_names); idx_s = ods.sheets.shtidx(wsh) ; ## First position after last sheet idx_e = idx_s - 1; rawarr = {}; lims = []; elseif (ods.changed < 2) idx_s = ods.sheets.shtidx(wsh); idx_e = ods.sheets.shtidx(wsh+1) - 1; ## Get all data in sheet and row/column limits [rawarr, ods] = __OCT_ods2oct__ (ods, wsh, "", struct ("formulas_as_text", 1)); lims = ods.limits; elseif (ods.changed >= 2) idx_s = ods.sheets.shtidx(wsh); idx_e = ods.sheets.shtidx(wsh+1) - 1; rawarr = {}; lims = []; endif ## Merge old and new data. Provisionally allow empty new data to wipe old data [rawarr, lims, onr, onc] = __OCT_merge_data__ (rawarr, lims, obj, obj_dims); ## D. Get default table/row/column styles ## Open file content.xml ## ... but first get original file size cxfs = dir ([ods.workbook filesep "content.xml"]); cxfs = cxfs.bytes; fids = fopen (sprintf ("%s/content.xml", ods.workbook), "r"); ## Get first part, til first sheet contnt = fread (fids, ods.sheets.shtidx(1)-1, "char=>char").'; fclose (fids); ## Get default styles stuff contnt = getxmlnode (contnt, "office:automatic-styles"); is = 1; stylenode = " "; ## Any non-empty value ## Usual default style names styles = struct ("tbl", "ta1", "row", "ro1", "col", "co1"); ## Get actual default styles while (! isempty (stylenode)) [stylenode, ~, is] = getxmlnode (contnt, "style:style", is, 1); stylefam = getxmlattv (stylenode, "style:family"); if (strcmpi (stylefam, "table-column")) style.col = stylefam; elseif (strcmpi (stylefam, "table")) styles.tbl = stylefam; elseif (strcmpi (stylefam, "table-row")) styles.row = stylefam; endif endwhile ## E. Create a temporary file to hold the new sheet xml ## Open sheet file (new or old) tmpfil = tempname; fid = fopen (tmpfil, "w+"); if (fid < 0) error ("oct2xls/ods: unable to write to file %s", tmpfil); endif ## Write data to sheet (actually table:table section in content.xml) status = __OCT__oct2ods_sh__ (fid, rawarr, wsh, lims, onc, onr, ... ods.sheets.sh_names{wsh}, styles); ## F. Merge new/updated sheet into content.xml ## Read first chunk of content.xml until sht_idx fidc = fopen (sprintf ("%s/content.xml", ods.workbook), "r+"); ## Go to start of requested sheet fseek (fidc, 0, "bof"); ## Read and concatenate just adapted/created sheet/table:table content_xml = fread (fidc, idx_s - 1, "char=>char").'; ## Rewind sheet and read it behind content_xml fseek (fid, 0, "bof"); sheet = fread (fid, Inf, "char=>char").'; lsheet = length (sheet); ## Close & delete sheet file fclose (fid); delete (tmpfil); content_xml = [ content_xml sheet] ; ## Read rest of content.xml, optionally delete overwritten sheet/table:table fseek (fidc, idx_e, "bof"); content_xml = [ content_xml fread(fidc, Inf, "char=>char").' ]; ## Write updated content.xml back to disk. fseek (fidc, 0, "bof"); fprintf (fidc, "%s", content_xml); cxbs = ftell (fidc); if (cxbs < cxfs) ## New content.xml shorter than original ==> overwrite trailing junk fprintf (fidc, "%s", repmat (" ", 1, cxfs - cxbs)); endif fclose (fidc); ## F. Update sheet pointers in ods file pointer if (new_sh) ods.sheets.shtidx(wsh+1) = idx_s + lsheet; ods.changed = 2; else offset = lsheet - (idx_e - idx_s) - 1; ods.sheets.shtidx(wsh+1 : end) += offset; endif ods.changed = max (ods.changed, 1); ## FIXME: Perhaps we'd need to update the metadata in meta.xml endfunction ## =========================================================================== function [ status ] = __OCT__oct2ods_sh__ (fid, rawarr, wsh, lims, onc, onr, tname, styles) ## Write out the lot to requested sheet ## 1. Sheet open tag fprintf (fid, '', tname, styles.tbl); ## 2. Default column styles. If required add columns repeated tag if (lims(1, 2) > 1) tncr = sprintf (' table:number-columns-repeated="%d"', lims(1, 2)); else tncr = ""; endif fprintf (fid, '', ... styles.col, tncr); ## 3. Spreadsheet rows ii = 1; while (ii <= onr) ## 3.1 Check for empty rows if (ii == 1) tnrr = lims(2, 1) - 1; else tnrr = 0; endif ## Check for consecutive empty rows. FIXME check for finite values as well while (ii <= onr && all (cellfun ("isempty", rawarr(ii, :)))) ++tnrr; ++ii; endwhile if (tnrr > 1) tnrrt = sprintf (' table:number-rows-repeated="%d"', tnrr); else tnrrt = ""; endif if (tnrr) ## Write empty row, optionally with row repeat tag if (lims (1, 2) > 1) ## Add number of empty columns repeat tag tcell = sprintf ('', ... lims(1, 2)); else tcell = ""; endif fprintf (fid, '%s', tnrrt, tcell); endif if (ii <= onr) ## Write table row opening tag fprintf (fid, '', styles.row); ## 3.2 Value cells jj = 1; while (jj <= onc) ## 3.2.1 Check if empty. Include empty columns left of rawarr if (jj == 1) tncr = lims(1, 1) - 1; else tncr = 0; endif ## Check consecutive empty cells & adapt ncr attr, write empty cells while (jj <= onc && (isempty (rawarr{ii, jj}) || ... (isnumeric (rawarr{ii, jj}) && ! isfinite (rawarr{ii, jj})))) ++tncr; ++jj; endwhile if (tncr > 1) fprintf (fid, '', tncr); elseif (tncr == 1) fprintf (fid, ""); endif ## Process non-empty data cells if (jj <= onc) ## 3.2.2 Non-empty cell. Determine value type. Set formula attr = empty of = ""; switch class (rawarr{ii, jj}) case {"double", "single"} if (isfinite (rawarr{ii, jj})) ovt = ' office:value-type="float"'; val = sprintf ("%.4f", rawarr{ii, jj}); txt = sprintf ("%s", val); ## Convert to attribute val = sprintf (' office:value="%0.15g"', rawarr{ii, jj}); else ovt = ""; val = ""; txt = ""; endif case {"int64", "int32", "int16", "int8", "uint64", "uint32", "uint16", "uint8"} ovt = ' office:value-type="integer"'; val = strtrim (sprintf ("%d15", rawarr{ii, jj})); txt = sprintf ("%s", val); ## Convert to attribute val = sprintf (' office:value="%s"', val); case "logical" ovt = ' office:value-type="boolean"'; val = "false"; if (rawarr{ii, jj}) val = "true"; endif txt = sprintf ("%s", upper (val)); ## Convert to attribute val = sprintf (' office:boolean-value="%s"', val); case "char" if (rawarr{ii, jj}(1) == "=") ## We guess a formula. Prepare formula attribute ovt = ""; txt = ""; val = ""; of = sprintf (' table:formula="of:%s"', rawarr{ii, jj}); ## FIXME We'd need to parse cell types in formula = may be formulas as well ## We cannot know, or parse, the resulting cell type, omit type info & text else ## Plain text ovt = ' office:value-type="string"'; val = strsplit (rawarr{ii, jj}, "\n"); txt = ""; for kk=1:numel (val) txt = [txt sprintf('%s', val{kk})]; endfor ## Convert to attribute val = ""; endif otherwise ## Unknown, illegal or otherwise unrecognized value ovt = ""; val = ""; txt = ""; endswitch # write out table-cell w office-value-type / office:value fprintf (fid, "%s", of, ovt, val, txt); endif ++jj; endwhile ## Write table row closing tag fprintf (fid, ""); endif ++ii; endwhile ## 4. Closing tag fprintf (fid, ""); status = 1; endfunction