class Xlsxrb::Ooxml::WorkbookWriter
Orchestrates writing a complete XLSX workbook. Generates all required OpenXML parts and assembles them into a ZIP.
Constants
- CT_NS
- DOC_REL
- REL_NS
- SSML_NS
Public Class Methods
# File lib/xlsxrb/ooxml/workbook_writer.rb, line 43 def initialize(sheets:, shared_strings: [], shared_strings_index: nil, styles: nil, defined_names: nil, core_properties: nil, app_properties: nil, custom_properties: nil, workbook_protection: nil) @sheets = sheets @shared_strings = shared_strings @shared_strings_index = shared_strings_index @styles = styles || {} @defined_names = defined_names || [] @core_properties = core_properties || {} @app_properties = app_properties || {} @custom_properties = custom_properties || [] @workbook_protection = workbook_protection @drawing_count = 0 @chart_count = 0 @comment_count = 0 @table_count = 0 @pivot_cache_count = 0 @pivot_table_count = 0 end
# File lib/xlsxrb/ooxml/workbook_writer.rb, line 31 def self.write(target, sheets:, shared_strings: [], shared_strings_index: nil, styles: nil, defined_names: nil, core_properties: nil, app_properties: nil, custom_properties: nil, workbook_protection: nil) Xlsxrb::TRACER.in_span("Ooxml::WorkbookWriter.write") do writer = new(sheets: sheets, shared_strings: shared_strings, shared_strings_index: shared_strings_index, styles: styles, defined_names: defined_names, core_properties: core_properties, app_properties: app_properties, custom_properties: custom_properties, workbook_protection: workbook_protection) writer.write_to(target) end end
Public Instance Methods
Source
# File lib/xlsxrb/ooxml/workbook_writer.rb, line 63 def write_to(target) preprocess_conditional_formats! ZipWriter.open(target) do |zip| zip.add_entry("[Content_Types].xml", build_content_types) zip.add_entry("_rels/.rels", build_root_rels) zip.add_entry("xl/workbook.xml", build_workbook_xml) zip.add_entry("xl/_rels/workbook.xml.rels", build_workbook_rels) zip.add_entry("xl/styles.xml", build_styles_xml) zip.add_entry("xl/sharedStrings.xml", build_shared_strings_xml) unless @shared_strings.empty? # Document properties zip.add_entry("docProps/core.xml", build_core_properties_xml) unless @core_properties.empty? zip.add_entry("docProps/app.xml", build_app_properties_xml) unless @app_properties.empty? zip.add_entry("docProps/custom.xml", build_custom_properties_xml) unless @custom_properties.empty? @sheets.each_with_index do |sheet, idx| sheet_images = sheet[:images] || [] sheet_charts = sheet[:charts] || [] sheet_shapes = sheet[:shapes] || [] sheet_comments = sheet[:comments] || [] sheet_tables = sheet[:tables] || [] sheet_hyperlinks = sheet[:hyperlinks] || [] has_drawing = sheet_images.any? || sheet_charts.any? || sheet_shapes.any? has_comments = sheet_comments.any? # Track relationship IDs for this sheet sheet_rels = [] drawing_rid = nil vml_rid = nil table_start_rid = nil hyperlink_rels = [] # Drawing relationships (images, charts, shapes) if has_drawing @drawing_count += 1 drawing_rid_num = sheet_rels.size + 1 sheet_rels << { id: "rId#{drawing_rid_num}", type: "#{DOC_REL}/drawing", target: "../drawings/drawing#{@drawing_count}.xml" } drawing_rid = "rId#{drawing_rid_num}" drawing_rels_data = [] drawing_parts = [] chart_writer = Xlsxrb::Ooxml::Writer.new chart_writer.add_sheet(sheet[:name]) unless sheet[:name] == "Sheet1" # Populate sheet data in chart_writer for chart cache resolution (sheet[:rows] || []).each do |row| cells = row.is_a?(Hash) ? row[:cells] : row.cells (cells || []).each do |cell| ref = cell.is_a?(Hash) ? cell[:ref] : cell.ref value = cell.is_a?(Hash) ? cell[:value] : cell.value chart_writer.set_cell(ref, value, sheet: sheet[:name]) end end sheet[:cells]&.each do |ref, value| chart_writer.set_cell(ref, value, sheet: sheet[:name]) end sheet_images.each do |img| media_idx = @drawing_count # simplified media_path = "xl/media/image#{media_idx}.#{img[:ext] || "png"}" zip.add_binary_entry(media_path, img[:file_data]) drawing_rels_data << { type: :image, target: "../media/image#{media_idx}.#{img[:ext] || "png"}" } drawing_parts << { kind: :pic, img: img, rid_index: drawing_rels_data.size } end sheet_charts.each do |chart_options| chart_writer.add_chart(**chart_options) end processed_charts = chart_writer.charts processed_charts.each do |chart| @chart_count += 1 chart_path = "xl/charts/chart#{@chart_count}.xml" zip.add_entry(chart_path, chart_writer.send(:generate_chart_xml, chart)) drawing_rels_data << { type: :chart, target: "../charts/chart#{@chart_count}.xml" } drawing_parts << { kind: :chart, chart: chart, rid_index: drawing_rels_data.size } end sheet_shapes.each_with_index do |shape, si| drawing_parts << { kind: :sp, shape: shape, id: drawing_parts.size + si + 2 } end drawing_xml = chart_writer.send(:generate_drawing_xml, drawing_parts) zip.add_entry("xl/drawings/drawing#{@drawing_count}.xml", drawing_xml) unless drawing_rels_data.empty? drawing_rels_xml = chart_writer.send(:generate_drawing_rels, drawing_rels_data) zip.add_entry("xl/drawings/_rels/drawing#{@drawing_count}.xml.rels", drawing_rels_xml) end end # Comment relationships if has_comments @comment_count += 1 comment_rid_num = sheet_rels.size + 1 sheet_rels << { id: "rId#{comment_rid_num}", type: "#{DOC_REL}/comments", target: "../comments#{@comment_count}.xml" } vml_rid_num = sheet_rels.size + 1 sheet_rels << { id: "rId#{vml_rid_num}", type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing", target: "../drawings/vmlDrawing#{@comment_count}.vml" } vml_rid = "rId#{vml_rid_num}" # Generate comments XML using Writer's method comment_writer = Xlsxrb::Ooxml::Writer.new sheet_comments.each do |c| comment_writer.add_comment(c[:cell], c[:text], author: c[:author] || "Author") end zip.add_entry("xl/comments#{@comment_count}.xml", comment_writer.send(:generate_comments_xml, comment_writer.comments)) zip.add_entry("xl/drawings/vmlDrawing#{@comment_count}.vml", comment_writer.send(:generate_vml_drawing_xml, comment_writer.comments)) end # Hyperlink relationships (external URLs need rels) sheet_hyperlinks.each_with_index do |link, _hi| next unless link[:url] h_rid_num = sheet_rels.size + 1 sheet_rels << { id: "rId#{h_rid_num}", type: "#{DOC_REL}/hyperlink", target: link[:url], target_mode: "External" } hyperlink_rels << h_rid_num end # Assign rIds to hyperlinks h_rid_idx = 0 enriched_hyperlinks = sheet_hyperlinks.map do |link| if link[:url] rid = hyperlink_rels[h_rid_idx] h_rid_idx += 1 link.merge(_rid: rid) else link end end # Table relationships unless sheet_tables.empty? table_start_rid = sheet_rels.size + 1 sheet_tables.each_with_index do |_tbl, _ti| @table_count += 1 t_rid_num = sheet_rels.size + 1 sheet_rels << { id: "rId#{t_rid_num}", type: "#{DOC_REL}/table", target: "../tables/table#{@table_count}.xml" } end end # Pivot table relationships sheet_pivot_tables = sheet[:pivot_tables] || [] pivot_table_start_cache = @pivot_cache_count unless sheet_pivot_tables.empty? pivot_writer = Xlsxrb::Ooxml::Writer.new pivot_writer.add_sheet(sheet[:name]) sheet_pivot_tables.each do |pt| pivot_writer.add_pivot_table( pt[:source_ref], row_fields: pt[:row_fields], data_fields: pt[:data_fields], col_fields: pt[:col_fields] || [], dest_ref: pt[:dest_ref] || "E1", name: pt[:name], field_names: pt[:field_names], items: pt[:items], sheet: sheet[:name] ) @pivot_cache_count += 1 @pivot_table_count += 1 pt_rid_num = sheet_rels.size + 1 sheet_rels << { id: "rId#{pt_rid_num}", type: "#{DOC_REL}/pivotTable", target: "../pivotTables/pivotTable#{@pivot_table_count}.xml" } end # Generate pivot table XML files pivot_data = pivot_writer.pivot_tables(sheet: sheet[:name]) pivot_data.each_with_index do |pt_data, pi| cache_idx = pivot_table_start_cache + pi + 1 pt_idx = pivot_table_start_cache + pi + 1 zip.add_entry("xl/pivotCache/pivotCacheDefinition#{cache_idx}.xml", pivot_writer.send(:generate_pivot_cache_definition_xml, pt_data, cache_idx)) zip.add_entry("xl/pivotCache/pivotCacheRecords#{cache_idx}.xml", pivot_writer.send(:generate_pivot_cache_records_xml, pt_data)) zip.add_entry("xl/pivotTables/pivotTable#{pt_idx}.xml", pivot_writer.send(:generate_pivot_table_xml, pt_data, cache_idx)) zip.add_entry("xl/pivotCache/_rels/pivotCacheDefinition#{cache_idx}.xml.rels", pivot_writer.send(:generate_pivot_cache_rels, cache_idx)) zip.add_entry("xl/pivotTables/_rels/pivotTable#{pt_idx}.xml.rels", pivot_writer.send(:generate_pivot_table_rels, cache_idx)) end end # Build worksheet rels if any zip.add_entry("xl/worksheets/_rels/sheet#{idx + 1}.xml.rels", build_sheet_rels_from_list(sheet_rels)) unless sheet_rels.empty? # Generate table XML files table_id_base = @table_count - sheet_tables.size sheet_tables.each_with_index do |tbl, ti| tbl_id = table_id_base + ti + 1 zip.add_entry("xl/tables/table#{tbl_id}.xml", build_table_xml(tbl, tbl_id)) end # Build the worksheet XML with all metadata zip.start_entry("xl/worksheets/sheet#{idx + 1}.xml") write_worksheet_xml( ZipEntryIO.new(zip), sheet, drawing_rid: drawing_rid, sheet_protection: sheet[:sheet_protection], auto_filter: sheet[:auto_filter], filter_columns: sheet[:filter_columns], sort_state: sheet[:sort_state], merge_cells: sheet[:merge_cells], conditional_formats: sheet[:conditional_formats], data_validations: sheet[:data_validations], hyperlinks: enriched_hyperlinks.empty? ? nil : enriched_hyperlinks, print_options: sheet[:print_options], page_margins: sheet[:page_margins], page_setup: sheet[:page_setup], header_footer: sheet[:header_footer], row_breaks: sheet[:row_breaks], col_breaks: sheet[:col_breaks], freeze_pane: sheet[:freeze_pane], split_pane: sheet[:split_pane], selection: sheet[:selection], sheet_view: sheet[:sheet_view], sheet_properties: sheet[:sheet_properties], tables: sheet_tables, table_start_rid: table_start_rid, legacy_drawing_rid: vml_rid, sparkline_groups: sheet[:sparkline_groups] ) zip.finish_entry end end end