From 60c1557e944e369d2185457420418b6b8b13f0a6 Mon Sep 17 00:00:00 2001 From: Nicholas Scheurich Date: Thu, 26 Mar 2020 11:24:21 -0500 Subject: [PATCH 1/4] Ignore Elixir LS cache --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 037b627..6a88aea 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ doc *.pdf *.swp + +# Elixir LS cache +/.elixir_ls/ \ No newline at end of file From b3344902e277feb26dee1364d9ce75d7f19e0985 Mon Sep 17 00:00:00 2001 From: Nicholas Scheurich Date: Thu, 26 Mar 2020 11:51:43 -0500 Subject: [PATCH 2/4] Add image support This adds support for embedding images in workbooks. It technically does two main things: - Implements the changes introduced by [proactively's image support branch](https://github.com/proactively/elixlsx/tree/image-support-no-reformat) on top of the most recent commit to master - Adds `width` and `height` options to `Elixlsx.Image` so that it's possible to configure how many columns/rows and image should occupy --- example.exs | 13 ++ ladybug-3475779_640.jpg | Bin 0 -> 35369 bytes lib/elixlsx/compiler.ex | 43 ++++++- lib/elixlsx/compiler/drawing_comp_info.ex | 24 ++++ lib/elixlsx/compiler/drawing_db.ex | 56 +++++++++ lib/elixlsx/compiler/workbook_comp_info.ex | 6 +- lib/elixlsx/image.ex | 72 +++++++++++ lib/elixlsx/sheet.ex | 48 +++++++- lib/elixlsx/writer.ex | 81 ++++++++++++- lib/elixlsx/xml_templates.ex | 134 +++++++++++++++++++++ mix.lock | 20 +-- 11 files changed, 481 insertions(+), 16 deletions(-) create mode 100644 ladybug-3475779_640.jpg create mode 100644 lib/elixlsx/compiler/drawing_comp_info.ex create mode 100644 lib/elixlsx/compiler/drawing_db.ex create mode 100644 lib/elixlsx/image.ex diff --git a/example.exs b/example.exs index c53cde1..38df373 100755 --- a/example.exs +++ b/example.exs @@ -136,7 +136,20 @@ sheet6 = # nest further |> Sheet.group_cols("C", "D") +# Images +sheet7 = %Sheet{ + name: "Images", + rows: List.duplicate(["A", "B", "C", "D", "E"], 5) +} + +sheet7 = + sheet7 + |> Sheet.insert_image(0, 5, "ladybug-3475779_640.jpg") + |> Sheet.set_row_height(1, 40) + |> Sheet.insert_image(6, 6, "ladybug-3475779_640.jpg") + Workbook.append_sheet(workbook, sheet4) |> Workbook.append_sheet(sheet5) |> Workbook.append_sheet(sheet6) +|> Workbook.append_sheet(sheet7) |> Elixlsx.write_to("example.xlsx") diff --git a/ladybug-3475779_640.jpg b/ladybug-3475779_640.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bcbacc446a002d8cec95067cbbf9d232baf93aa4 GIT binary patch literal 35369 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 literal 0 HcmV?d00001 diff --git a/lib/elixlsx/compiler.ex b/lib/elixlsx/compiler.ex index 0f47291..e79b07a 100644 --- a/lib/elixlsx/compiler.ex +++ b/lib/elixlsx/compiler.ex @@ -1,8 +1,10 @@ defmodule Elixlsx.Compiler do alias Elixlsx.Compiler.WorkbookCompInfo alias Elixlsx.Compiler.SheetCompInfo + alias Elixlsx.Compiler.DrawingCompInfo alias Elixlsx.Compiler.CellStyleDB alias Elixlsx.Compiler.StringDB + alias Elixlsx.Compiler.DrawingDB alias Elixlsx.Sheet @doc ~S""" @@ -23,6 +25,28 @@ defmodule Elixlsx.Compiler do {Enum.reverse(sheetCompInfos), nextrID} end + @doc ~S""" + Accepts a list of Sheets and the next free relationship ID. + Returns a tuple containing a list of DrawingCompInfo's based on the images + within the sheets and the next free relationship ID. + """ + @spec make_drawing_info(nonempty_list(Sheet.t()), non_neg_integer) :: + {list(DrawingCompInfo.t()), non_neg_integer} + def make_drawing_info(sheets, init_rId) do + # fold helper. aggregator holds {list(DrawingCompInfo), drawingidx, rId}. + add_sheet = fn sheet, {dci, idx, rId} -> + if sheet.images == [] do + {dci, idx, rId} + else + {[DrawingCompInfo.make(idx, rId) | dci], idx + 1, rId + 1} + end + end + + # TODO probably better to use a zip [1..] |> map instead of fold[l|r]/reverse + {sheetCompInfos, _, nextrID} = List.foldl(sheets, {[], 1, init_rId}, add_sheet) + {Enum.reverse(sheetCompInfos), nextrID} + end + def compinfo_cell_pass_value(wci, value) do cond do is_binary(value) && XML.valid?(value) -> @@ -57,6 +81,19 @@ defmodule Elixlsx.Compiler do end end + def compinfo_image_pass(wci, image) do + update_in( + wci.drawingdb, + &DrawingDB.register_image(&1, image) + ) + end + + def compinfo_from_images(wci, images) do + List.foldl(images, wci, fn image, wci -> + compinfo_image_pass(wci, image) + end) + end + @spec compinfo_from_rows(WorkbookCompInfo.t(), list(list(any()))) :: WorkbookCompInfo.t() def compinfo_from_rows(wci, rows) do List.foldl(rows, wci, fn cols, wci -> @@ -69,16 +106,20 @@ defmodule Elixlsx.Compiler do @spec compinfo_from_sheets(WorkbookCompInfo.t(), list(Sheet.t())) :: WorkbookCompInfo.t() def compinfo_from_sheets(wci, sheets) do List.foldl(sheets, wci, fn sheet, wci -> - compinfo_from_rows(wci, sheet.rows) + wci + |> compinfo_from_rows(sheet.rows) + |> compinfo_from_images(sheet.images) end) end @first_free_rid 2 def make_workbook_comp_info(workbook) do {sci, next_rId} = make_sheet_info(workbook.sheets, @first_free_rid) + {dci, next_rId} = make_drawing_info(workbook.sheets, next_rId) %WorkbookCompInfo{ sheet_info: sci, + drawing_info: dci, next_free_xl_rid: next_rId } |> compinfo_from_sheets(workbook.sheets) diff --git a/lib/elixlsx/compiler/drawing_comp_info.ex b/lib/elixlsx/compiler/drawing_comp_info.ex new file mode 100644 index 0000000..78082e8 --- /dev/null +++ b/lib/elixlsx/compiler/drawing_comp_info.ex @@ -0,0 +1,24 @@ +defmodule Elixlsx.Compiler.DrawingCompInfo do + alias Elixlsx.Compiler.DrawingCompInfo + + @moduledoc ~S""" + Compilation info for a Drawing, to be filled during the actual + write process. + """ + defstruct rId: "", filename: "drawing1.xml", drawingId: 0 + + @type t :: %DrawingCompInfo{ + rId: String.t(), + filename: String.t(), + drawingId: non_neg_integer + } + + @spec make(non_neg_integer, non_neg_integer) :: DrawingCompInfo.t() + def make(drawingidx, rId) do + %DrawingCompInfo{ + rId: "rId" <> to_string(rId), + filename: "drawing" <> to_string(drawingidx) <> ".xml", + drawingId: drawingidx + } + end +end diff --git a/lib/elixlsx/compiler/drawing_db.ex b/lib/elixlsx/compiler/drawing_db.ex new file mode 100644 index 0000000..ade049a --- /dev/null +++ b/lib/elixlsx/compiler/drawing_db.ex @@ -0,0 +1,56 @@ +defmodule Elixlsx.Compiler.DrawingDB do + alias __MODULE__ + alias Elixlsx.Compiler.DBUtil + alias Elixlsx.Image + + @doc """ + Database of drawing elements in the whole document. drawing id values must be + unique across the document regardless of what kind of drawing they are. + So far this only supports images, but could be extended to include other + kinds of drawing. + An alternative would be to add a Drawing module and have "subclasses" for + different drawing types + """ + + defstruct images: %{}, element_count: 0 + + @type t :: %DrawingDB{ + images: %{Image.t() => pos_integer}, + element_count: non_neg_integer + } + + def register_image(drawingdb, image) do + case Map.fetch(drawingdb.images, image) do + :error -> + %DrawingDB{ + images: Map.put(drawingdb.images, image, drawingdb.element_count + 1), + element_count: drawingdb.element_count + 1 + } + + {:ok, _} -> + drawingdb + end + end + + def get_id(drawingdb, image) do + case Map.fetch(drawingdb.images, image) do + :error -> + raise %ArgumentError{ + message: "Invalid key provided for DrawingDB.get_id: " <> inspect(image) + } + + {:ok, id} -> + id + end + end + + def id_sorted_drawings(db), do: DBUtil.id_sorted_values(db.images) + + def image_types(db) do + db.images + |> Enum.reduce(%MapSet{}, fn {i, _}, acc -> + MapSet.put(acc, {i.extension, i.type}) + end) + |> Enum.to_list() + end +end diff --git a/lib/elixlsx/compiler/workbook_comp_info.ex b/lib/elixlsx/compiler/workbook_comp_info.ex index a883a96..4b8bf2d 100644 --- a/lib/elixlsx/compiler/workbook_comp_info.ex +++ b/lib/elixlsx/compiler/workbook_comp_info.ex @@ -6,25 +6,29 @@ defmodule Elixlsx.Compiler.WorkbookCompInfo do required to generate the XML file. It is used as the aggregator when folding over the individual - cells. + cells and images. """ defstruct sheet_info: nil, + drawing_info: nil, stringdb: %Compiler.StringDB{}, fontdb: %Compiler.FontDB{}, filldb: %Compiler.FillDB{}, cellstyledb: %Compiler.CellStyleDB{}, numfmtdb: %Compiler.NumFmtDB{}, borderstyledb: %Compiler.BorderStyleDB{}, + drawingdb: %Compiler.DrawingDB{}, next_free_xl_rid: nil @type t :: %Compiler.WorkbookCompInfo{ sheet_info: [Compiler.SheetCompInfo.t()], + drawing_info: [Compiler.DrawingCompInfo.t()], stringdb: Compiler.StringDB.t(), fontdb: Compiler.FontDB.t(), filldb: Compiler.FillDB.t(), cellstyledb: Compiler.CellStyleDB.t(), numfmtdb: Compiler.NumFmtDB.t(), borderstyledb: Compiler.BorderStyleDB.t(), + drawingdb: Compiler.DrawingDB.t(), next_free_xl_rid: non_neg_integer } end diff --git a/lib/elixlsx/image.ex b/lib/elixlsx/image.ex new file mode 100644 index 0000000..0600bf4 --- /dev/null +++ b/lib/elixlsx/image.ex @@ -0,0 +1,72 @@ +defmodule Elixlsx.Image do + alias Elixlsx.Image + + @moduledoc ~S""" + Structure for excel drawing files. + - x_offset: integer + - y_offset: integer + - x_scale: float + - y_scale: float + - positioning: atom (:absolute, :oneCell, :twoCell) + - width: integer + - height: integer + """ + + defstruct file_path: "", + type: "image/png", + extension: "png", + rowidx: 0, + colidx: 0, + x_offset: 0, + y_offset: 0, + x_scale: 1, + y_scale: 1, + positioning: :twoCell, + width: 1, + height: 1 + + @type t :: %Image{ + file_path: String.t(), + type: String.t(), + extension: String.t(), + rowidx: integer, + colidx: integer, + x_offset: integer, + y_offset: integer, + x_scale: float, + y_scale: float, + positioning: atom, + width: integer, + height: integer + } + + @doc """ + Create an image struct based on opts + """ + def new(file_path, rowidx, colidx, opts \\ []) do + {ext, type} = image_type(file_path) + + %Image{ + file_path: file_path, + type: type, + extension: ext, + rowidx: rowidx, + colidx: colidx, + x_offset: Keyword.get(opts, :x_offset, 0), + y_offset: Keyword.get(opts, :y_offset, 0), + x_scale: Keyword.get(opts, :x_scale, 1), + y_scale: Keyword.get(opts, :y_scale, 1), + positioning: Keyword.get(opts, :positioning, :twoCell), + width: Keyword.get(opts, :width, 1), + height: Keyword.get(opts, :height, 1) + } + end + + defp image_type(file_path) do + case Path.extname(file_path) do + ".jpg" -> {"jpg", "image/jpeg"} + ".jpeg" -> {"jpeg", "image/jpeg"} + ".png" -> {"png", "image/png"} + end + end +end diff --git a/lib/elixlsx/sheet.ex b/lib/elixlsx/sheet.ex index a426991..36c5f48 100644 --- a/lib/elixlsx/sheet.ex +++ b/lib/elixlsx/sheet.ex @@ -1,6 +1,7 @@ defmodule Elixlsx.Sheet do alias __MODULE__ alias Elixlsx.Sheet + alias Elixlsx.Image alias Elixlsx.Util @moduledoc ~S""" @@ -19,6 +20,7 @@ defmodule Elixlsx.Sheet do """ defstruct name: "", rows: [], + images: [], col_widths: %{}, row_heights: %{}, group_cols: [], @@ -30,6 +32,7 @@ defmodule Elixlsx.Sheet do @type t :: %Sheet{ name: String.t(), rows: list(list(any())), + images: list(Image.t()), col_widths: %{pos_integer => number}, row_heights: %{pos_integer => number}, group_cols: list(rowcol_group), @@ -95,7 +98,8 @@ defmodule Elixlsx.Sheet do set_at(sheet, row, col, content, opts) end - @spec set_at(Sheet.t(), non_neg_integer, non_neg_integer, any(), Keyword.t()) :: Sheet.t() + @spec set_at(Sheet.t(), non_neg_integer, non_neg_integer, any(), Keyword.t()) :: + Sheet.t() @doc ~S""" Set a cell at a given row/column index. Indizes start at 0. @@ -110,6 +114,8 @@ defmodule Elixlsx.Sheet do """ def set_at(sheet, rowidx, colidx, content, opts \\ []) when is_number(rowidx) and is_number(colidx) do + sheet = maybe_extend(sheet, rowidx, colidx) + cond do length(sheet.rows) <= rowidx -> # append new rows, call self again with new sheet @@ -136,6 +142,30 @@ defmodule Elixlsx.Sheet do end end + @spec maybe_extend(Sheet.t(), non_neg_integer, non_neg_integer) :: Sheet.t() + defp maybe_extend(sheet, rowidx, colidx) do + cond do + length(sheet.rows) <= rowidx -> + # append new rows, call self again with new sheet + n_new_rows = rowidx - length(sheet.rows) + new_rows = 0..n_new_rows |> Enum.map(fn _ -> [] end) + + update_in(sheet.rows, &(&1 ++ new_rows)) + |> maybe_extend(rowidx, colidx) + + length(Enum.at(sheet.rows, rowidx)) <= colidx -> + n_new_cols = colidx - length(Enum.at(sheet.rows, rowidx)) + new_cols = 0..n_new_cols |> Enum.map(fn _ -> nil end) + new_row = Enum.at(sheet.rows, rowidx) ++ new_cols + + update_in(sheet.rows, &List.replace_at(&1, rowidx, new_row)) + |> maybe_extend(rowidx, colidx) + + true -> + sheet + end + end + @spec set_col_width(Sheet.t(), String.t(), number) :: Sheet.t() @doc ~S""" Set the column width for a given column. Column is indexed by @@ -205,4 +235,20 @@ defmodule Elixlsx.Sheet do def remove_pane_freeze(sheet) do %{sheet | pane_freeze: nil} end + + @doc """ + Insert an image at a given position. + """ + @spec insert_image(Sheet.t(), non_neg_integer, non_neg_integer, String.t(), key: any) :: + Sheet.t() + def insert_image(sheet, rowidx, colidx, imagepath, opts \\ []) + when is_number(rowidx) and is_number(colidx) do + image = Image.new(imagepath, rowidx, colidx, opts) + + # Ensure there are enough rows and columns to accomodate the image position + sheet = maybe_extend(sheet, rowidx, colidx) + + # Add the image to the list of images in this sheet + update_in(sheet.images, &[image | &1]) + end end diff --git a/lib/elixlsx/writer.ex b/lib/elixlsx/writer.ex index 76ac8a9..26fa7b8 100644 --- a/lib/elixlsx/writer.ex +++ b/lib/elixlsx/writer.ex @@ -2,6 +2,7 @@ defmodule Elixlsx.Writer do alias Elixlsx.Util, as: U alias Elixlsx.XMLTemplates alias Elixlsx.Compiler.StringDB + alias Elixlsx.Compiler.DrawingDB alias Elixlsx.Compiler.WorkbookCompInfo alias Elixlsx.Compiler.SheetCompInfo alias Elixlsx.Workbook @@ -101,13 +102,86 @@ defmodule Elixlsx.Writer do String.to_charlist("xl/worksheets/#{sci.filename}") end + @spec sheet_full__rels_path(SheetCompInfo.t()) :: list(char) + defp sheet_full__rels_path(sci) do + String.to_charlist("xl/worksheets/_rels/#{sci.filename}.rels") + end + + @spec get_xl_worksheets__rel_dir(Sheet.t(), SheetCompInfo.t()) :: list(zip_tuple) + def get_xl_worksheets__rel_dir(s, sci) do + if s.images == [] do + [] + else + [{sheet_full__rels_path(sci), XMLTemplates.make_xl_worksheet_rel_sheet()}] + end + end + @spec get_xl_worksheets_dir(Workbook.t(), WorkbookCompInfo.t()) :: list(zip_tuple) def get_xl_worksheets_dir(data, wci) do sheets = data.sheets Enum.zip(sheets, wci.sheet_info) - |> Enum.map(fn {s, sci} -> - {sheet_full_path(sci), XMLTemplates.make_sheet(s, wci)} + |> Enum.flat_map(fn {s, sci} -> + [{sheet_full_path(sci), XMLTemplates.make_sheet(s, wci)}] ++ + get_xl_worksheets__rel_dir(s, sci) + end) + end + + @spec drawing_full_path(DrawingCompInfo.t()) :: list(char) + defp drawing_full_path(dci) do + String.to_charlist("xl/drawings/#{dci.filename}") + end + + @spec drawing_full__rels_path(DrawingCompInfo.t()) :: list(char) + defp drawing_full__rels_path(dci) do + String.to_charlist("xl/drawings/_rels/#{dci.filename}.rels") + end + + @spec image_full_path(Image.t(), WorkbookCompInfo.t()) :: list(char) + def image_full_path(image, wci) do + id = DrawingDB.get_id(wci.drawingdb, image) + + String.to_charlist("xl/media/image#{id}.#{image.extension}") + end + + @spec read_image(String.t()) :: binary + def read_image(file_path) do + File.read!(file_path) + end + + @spec get_xl_drawings__rel_dir(list(Image.t()), DrawingCompInfo.t(), WorkbookCompInfo.t()) :: + list(zip_tuple) + def get_xl_drawings__rel_dir(images, dci, wci) do + if images == [] do + [] + else + [{drawing_full__rels_path(dci), XMLTemplates.make_xl_drawing_rel_sheet(images, wci)}] + end + end + + @spec get_xl_drawings_dir(Workbook.t(), WorkbookCompInfo.t()) :: list(zip_tuple) + def get_xl_drawings_dir(data, wci) do + ## We have one wci.drawing_info per sheet that has any images + has_images? = fn s -> s.images != [] end + sheets = for sheet <- data.sheets, has_images?.(sheet), do: sheet + + Enum.zip(sheets, wci.drawing_info) + |> Enum.flat_map(fn {s, dci} -> + [{drawing_full_path(dci), XMLTemplates.make_drawing(s.images, wci)}] ++ + get_xl_drawings__rel_dir(s.images, dci, wci) + end) + end + + @spec get_xl_media_dir(Workbook.t(), WorkbookCompInfo.t()) :: list(zip_tuple) + def get_xl_media_dir(data, wci) do + has_images? = fn s -> s.images != [] end + sheets = for sheet <- data.sheets, has_images?.(sheet), do: sheet + + sheets + |> Enum.flat_map(fn s -> + Enum.map(s.images, fn image -> + {image_full_path(image, wci), read_image(image.file_path)} + end) end) end @@ -125,6 +199,7 @@ defmodule Elixlsx.Writer do get_xl_workbook_xml(data, sheet_comp_infos) ] ++ get_xl_rels_dir(data, sheet_comp_infos, next_free_xl_rid) ++ - get_xl_worksheets_dir(data, wci) + get_xl_worksheets_dir(data, wci) ++ + get_xl_drawings_dir(data, wci) ++ get_xl_media_dir(data, wci) end end diff --git a/lib/elixlsx/xml_templates.ex b/lib/elixlsx/xml_templates.ex index fdd4e16..2c2f617 100644 --- a/lib/elixlsx/xml_templates.ex +++ b/lib/elixlsx/xml_templates.ex @@ -7,6 +7,7 @@ defmodule Elixlsx.XMLTemplates do alias Elixlsx.Compiler.SheetCompInfo alias Elixlsx.Compiler.NumFmtDB alias Elixlsx.Compiler.BorderStyleDB + alias Elixlsx.Compiler.DrawingDB alias Elixlsx.Compiler.WorkbookCompInfo alias Elixlsx.Style.CellStyle alias Elixlsx.Style.Font @@ -95,6 +96,19 @@ defmodule Elixlsx.XMLTemplates do }\"/>" end + @spec make_xl_worksheet_rel_sheet() :: String.t() + def make_xl_worksheet_rel_sheet() do + # We should probably care about a future with multiple rels here + # but for now just hard code the drawing one + """ + + + + + """ + end + @spec make_xl_rel_sheets(nonempty_list(SheetCompInfo.t())) :: String.t() def make_xl_rel_sheets(sheet_comp_infos) do Enum.map_join(sheet_comp_infos, &make_xl_rel_sheet/1) @@ -141,10 +155,32 @@ defmodule Elixlsx.XMLTemplates do Enum.map_join(sheet_comp_infos, &contenttypes_sheet_entry/1) end + defp contenttypes_drawing_entry(drawing_comp_info) do + """ + + """ + end + + defp contenttypes_drawing_entries(drawing_comp_infos) do + Enum.map_join(drawing_comp_infos, &contenttypes_drawing_entry/1) + end + + defp contenttypes_drawing_type({extension, type}) do + """ + + """ + end + + defp contenttypes_drawing_types(drawing_db) do + drawing_types = DrawingDB.image_types(drawing_db) + Enum.map_join(drawing_types, &contenttypes_drawing_type/1) + end + def make_contenttypes_xml(wci) do ~S""" + @@ -153,6 +189,8 @@ defmodule Elixlsx.XMLTemplates do """ <> contenttypes_sheet_entries(wci.sheet_info) <> + contenttypes_drawing_entries(wci.drawing_info) <> + contenttypes_drawing_types(wci.drawingdb) <> ~S""" @@ -432,6 +470,99 @@ defmodule Elixlsx.XMLTemplates do end end + ### + ### xl/drawings/drawing*.xml + ### + @spec make_drawing(list(Image.t()), WorkbookCompInfo.t()) :: String.t() + def make_drawing([], _wci), do: "" + + def make_drawing(images, wci) do + """ + + + """ <> + Enum.map_join(images, fn i -> make_xl_drawings_twoCell(i, wci) end) <> + """ + + """ + end + + defp make_xl_drawings_twoCell(image, wci) do + drawing_id = to_string(DrawingDB.get_id(wci.drawingdb, image)) + + """ + + + #{image.colidx} + #{image.x_offset} + #{image.rowidx} + #{image.x_offset} + + + #{image.colidx + image.width} + #{image.x_offset} + #{image.rowidx + image.height} + #{image.x_offset} + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ + end + + def xl_drawing_rel_sheet_rows(images, wci) do + Enum.map_join(images, fn image -> + id = DrawingDB.get_id(wci.drawingdb, image) + + ~s""" + + """ + end) + end + + @spec make_xl_drawing_rel_sheet(list(Image.t()), WorkbookCompInfo.t()) :: String.t() + def make_xl_drawing_rel_sheet(images, wci) do + ~S""" + + + """ <> + xl_drawing_rel_sheet_rows(images, wci) <> + ~S""" + + """ + end + + @spec make_drawing_ref(List.t()) :: String.t() + defp make_drawing_ref([]), do: "" + defp make_drawing_ref(_drawings), do: "" + @spec make_sheet(Sheet.t(), WorkbookCompInfo.t()) :: String.t() @doc ~S""" Returns the XML content for single sheet. @@ -474,6 +605,9 @@ defmodule Elixlsx.XMLTemplates do xl_merge_cells(sheet.merge_cells) <> """ + """ <> + make_drawing_ref(sheet.images) <> + """ """ end diff --git a/mix.lock b/mix.lock index 9af9fbe..e71185c 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,12 @@ %{ - "bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], [], "hexpm"}, - "credo": {:hex, :credo, "0.5.3", "0c405b36e7651245a8ed63c09e2d52c2e2b89b6d02b1570c4d611e0fcbecf4a2", [:mix], [{:bunt, "~> 0.1.6", [repo: "hexpm", hex: :bunt, optional: false]}], "hexpm"}, - "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], []}, - "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.19.2", "6f4081ccd9ed081b6dc0bd5af97a41e87f5554de469e7d76025fba535180565f", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, - "excheck": {:hex, :excheck, "0.5.3", "7326a29cc5fdb6900e66dac205a6a70cc994e2fe037d39136817d7dab13cdabf", [:mix], []}, - "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, - "triq": {:hex, :triq, "1.3.0", "d9ed60f3cd2b6bacbb721bc9873e67e07b02e5b97c63d40db35b12670a7f1bf4", [:rebar3], [], "hexpm"}, + "bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], [], "hexpm", "4fb7b2f7b04af13cf210b132f8d10db52d4a57d36cb974e8025d7fdb12ca97fc"}, + "credo": {:hex, :credo, "0.5.3", "0c405b36e7651245a8ed63c09e2d52c2e2b89b6d02b1570c4d611e0fcbecf4a2", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm", "90bca5180fe64c47343969ad3e32bf1edc18d929689e8c00c810655a7a391427"}, + "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm", "6c32a70ed5d452c6650916555b1f96c79af5fc4bf286997f8b15f213de786f73"}, + "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm", "000aaeff08919e95e7aea13e4af7b2b9734577b3e6a7c50ee31ee88cab6ec4fb"}, + "ex_doc": {:hex, :ex_doc, "0.19.2", "6f4081ccd9ed081b6dc0bd5af97a41e87f5554de469e7d76025fba535180565f", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "4eae888633d2937e0a8839ae6002536d459c22976743c9dc98dd05941a06c016"}, + "excheck": {:hex, :excheck, "0.5.3", "7326a29cc5fdb6900e66dac205a6a70cc994e2fe037d39136817d7dab13cdabf", [:mix], [], "hexpm", "2a27ffeff9d3b2ef45c454efb13990f08bc2578f93fd6d054025da74775ca869"}, + "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5fbc8e549aa9afeea2847c0769e3970537ed302f93a23ac612602e805d9d1e7f"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "adf0218695e22caeda2820eaba703fa46c91820d53813a2223413da3ef4ba515"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm", "5c040b8469c1ff1b10093d3186e2e10dbe483cd73d79ec017993fb3985b8a9b3"}, + "triq": {:hex, :triq, "1.3.0", "d9ed60f3cd2b6bacbb721bc9873e67e07b02e5b97c63d40db35b12670a7f1bf4", [:rebar3], [], "hexpm", "e01eb99fc53099ded985bb0c629ea0d2b0bfcf5b9a4178e0a93b08dbe51aa8cd"}, } From e5fc0808544fff9a5adb3a4923b7fbe8b1415459 Mon Sep 17 00:00:00 2001 From: Jose Ustra Date: Wed, 17 Mar 2021 17:31:36 +0000 Subject: [PATCH 3/4] fix offset --- lib/elixlsx/image.ex | 38 ++++++++++++++++++------------------ lib/elixlsx/xml_templates.ex | 12 ++++++------ 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/lib/elixlsx/image.ex b/lib/elixlsx/image.ex index 0600bf4..770e21d 100644 --- a/lib/elixlsx/image.ex +++ b/lib/elixlsx/image.ex @@ -3,10 +3,10 @@ defmodule Elixlsx.Image do @moduledoc ~S""" Structure for excel drawing files. - - x_offset: integer - - y_offset: integer - - x_scale: float - - y_scale: float + - x_from_offset: integer + - x_to_offset: integer + - y_from_offset: integer + - y_to_offset: integer - positioning: atom (:absolute, :oneCell, :twoCell) - width: integer - height: integer @@ -17,11 +17,11 @@ defmodule Elixlsx.Image do extension: "png", rowidx: 0, colidx: 0, - x_offset: 0, - y_offset: 0, - x_scale: 1, - y_scale: 1, - positioning: :twoCell, + x_from_offset: 0, + y_from_offset: 0, + x_to_offset: 0, + y_to_offset: 0, + positioning: "", width: 1, height: 1 @@ -31,11 +31,11 @@ defmodule Elixlsx.Image do extension: String.t(), rowidx: integer, colidx: integer, - x_offset: integer, - y_offset: integer, - x_scale: float, - y_scale: float, - positioning: atom, + x_from_offset: integer, + y_from_offset: integer, + x_to_offset: integer, + y_to_offset: integer, + positioning: atom | String.t(), width: integer, height: integer } @@ -52,11 +52,11 @@ defmodule Elixlsx.Image do extension: ext, rowidx: rowidx, colidx: colidx, - x_offset: Keyword.get(opts, :x_offset, 0), - y_offset: Keyword.get(opts, :y_offset, 0), - x_scale: Keyword.get(opts, :x_scale, 1), - y_scale: Keyword.get(opts, :y_scale, 1), - positioning: Keyword.get(opts, :positioning, :twoCell), + x_from_offset: Keyword.get(opts, :x_from_offset, 0), + y_from_offset: Keyword.get(opts, :y_from_offset, 0), + x_to_offset: Keyword.get(opts, :x_to_offset, 0), + y_to_offset: Keyword.get(opts, :y_to_offset, 0), + positioning: Keyword.get(opts, :positioning, ""), width: Keyword.get(opts, :width, 1), height: Keyword.get(opts, :height, 1) } diff --git a/lib/elixlsx/xml_templates.ex b/lib/elixlsx/xml_templates.ex index 2c2f617..db1fd1a 100644 --- a/lib/elixlsx/xml_templates.ex +++ b/lib/elixlsx/xml_templates.ex @@ -495,15 +495,15 @@ defmodule Elixlsx.XMLTemplates do #{image.colidx} - #{image.x_offset} + #{image.x_from_offset} #{image.rowidx} - #{image.x_offset} + #{image.y_from_offset} - #{image.colidx + image.width} - #{image.x_offset} - #{image.rowidx + image.height} - #{image.x_offset} + #{image.colidx} + #{image.x_to_offset} + #{image.rowidx} + #{image.y_to_offset} From 8532264eab9f9c58e535a3ec76b739411e904347 Mon Sep 17 00:00:00 2001 From: Jose Ustra Date: Wed, 15 Jun 2022 10:51:50 +0100 Subject: [PATCH 4/4] remove a bit of duplicated code --- lib/elixlsx/sheet.ex | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/lib/elixlsx/sheet.ex b/lib/elixlsx/sheet.ex index 6b146b9..c6f1ebd 100644 --- a/lib/elixlsx/sheet.ex +++ b/lib/elixlsx/sheet.ex @@ -128,18 +128,11 @@ defmodule Elixlsx.Sheet do cond do length(sheet.rows) <= rowidx -> # append new rows, call self again with new sheet - n_new_rows = rowidx - length(sheet.rows) - new_rows = 0..n_new_rows |> Enum.map(fn _ -> [] end) - - update_in(sheet.rows, &(&1 ++ new_rows)) + append_rows(sheet, rowidx) |> set_at(rowidx, colidx, content, opts) length(Enum.at(sheet.rows, rowidx)) <= colidx -> - n_new_cols = colidx - length(Enum.at(sheet.rows, rowidx)) - new_cols = 0..n_new_cols |> Enum.map(fn _ -> nil end) - new_row = Enum.at(sheet.rows, rowidx) ++ new_cols - - update_in(sheet.rows, &List.replace_at(&1, rowidx, new_row)) + replace_rows(sheet, rowidx, colidx) |> set_at(rowidx, colidx, content, opts) true -> @@ -156,18 +149,11 @@ defmodule Elixlsx.Sheet do cond do length(sheet.rows) <= rowidx -> # append new rows, call self again with new sheet - n_new_rows = rowidx - length(sheet.rows) - new_rows = 0..n_new_rows |> Enum.map(fn _ -> [] end) - - update_in(sheet.rows, &(&1 ++ new_rows)) + append_rows(sheet, rowidx) |> maybe_extend(rowidx, colidx) length(Enum.at(sheet.rows, rowidx)) <= colidx -> - n_new_cols = colidx - length(Enum.at(sheet.rows, rowidx)) - new_cols = 0..n_new_cols |> Enum.map(fn _ -> nil end) - new_row = Enum.at(sheet.rows, rowidx) ++ new_cols - - update_in(sheet.rows, &List.replace_at(&1, rowidx, new_row)) + replace_rows(sheet, rowidx, colidx) |> maybe_extend(rowidx, colidx) true -> @@ -175,6 +161,23 @@ defmodule Elixlsx.Sheet do end end + @spec append_rows(Sheet.t(), non_neg_integer) :: Sheet.t() + defp append_rows(sheet, rowidx) do + n_new_rows = rowidx - length(sheet.rows) + new_rows = 0..n_new_rows |> Enum.map(fn _ -> [] end) + + update_in(sheet.rows, &(&1 ++ new_rows)) + end + + @spec replace_rows(Sheet.t(), non_neg_integer, non_neg_integer) :: Sheet.t() + defp replace_rows(sheet, rowidx, colidx) do + n_new_cols = colidx - length(Enum.at(sheet.rows, rowidx)) + new_cols = 0..n_new_cols |> Enum.map(fn _ -> nil end) + new_row = Enum.at(sheet.rows, rowidx) ++ new_cols + + update_in(sheet.rows, &List.replace_at(&1, rowidx, new_row)) + end + @spec set_col_width(Sheet.t(), String.t(), number) :: Sheet.t() @doc ~S""" Set the column width for a given column.