Skip to content

Commit

Permalink
Images
Browse files Browse the repository at this point in the history
The previous work in xou#124 by @ngscheurich and @joseustra
was using a twoCellAnchor but that's quite tricky
to get right.

This is now using a oneCellAnchor to make the
calculations much simpler.

The api is almost the same, but you don't need to add
any emu based values for offsets. You can also align
the image from the right of a cell. That's something
I needed but you also have to take the max character
width into account.

Also added a few extra tests.

I also fixed the formula because I couldn't run
the example file without doing that first.
  • Loading branch information
krishandley committed Aug 25, 2023
1 parent a70d878 commit b9f47bc
Show file tree
Hide file tree
Showing 17 changed files with 1,033 additions and 92 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ elixlsx-*.tar

# Misc.
*.swp

# Elixir LS cache
/.elixir_ls/
65 changes: 59 additions & 6 deletions example.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ require Elixlsx

alias Elixlsx.{Sheet, Workbook}

sheet1 = Sheet.with_name("First")
sheet1 =
Sheet.with_name("First")
# Set cell B2 to the string "Hi". :)
|> Sheet.set_cell("B2", "Hi")
# Optionally, set font properties:
Expand Down Expand Up @@ -48,11 +49,15 @@ sheet1 = Sheet.with_name("First")
|> Sheet.set_cell("A3", "cow")
|> Sheet.add_data_validations("A1", "A10", ["dog", "cat", "cow"])
# within same sheet
|> Sheet.add_data_validations("A1", "A10", "=$A$2:$A$16")
|> Sheet.add_data_validations("B1", "B10", "=$B$2:$B$16")
# reference to other sheet "=#{sheet.name}!$A$2:$A$16"
|> Sheet.add_data_validations("A1", "A10", "=sheet2!$A$2:$A$16")
|> Sheet.add_data_validations("C1", "C10", "=Third!$A$2:$A$16")

workbook = %Workbook{sheets: [sheet1]}
workbook = %Workbook{
sheets: [sheet1],
font: "Arial",
font_size: 12
}

# it is also possible to add a custom "created" date to workbook, otherwise,
# the current date is used.
Expand All @@ -65,7 +70,7 @@ sheet2 =
name: "Third",
rows: [[1, 2, 3, 4, 5], [1, 2], ["increased row height"], ["hello", "world"]]
}
|> Sheet.set_row_height(3, 40)
|> Sheet.set_row_height(2, 75)

workbook = Workbook.append_sheet(workbook, sheet2)

Expand Down Expand Up @@ -136,7 +141,7 @@ sheet5 =
sheet6 =
%Sheet{
name: "Row and Column Groups",
rows: 1..100 |> Enum.chunk(10),
rows: 1..100 |> Enum.chunk_every(10),
# collapse and hide rows 2 to 3
group_rows: [{2..3, collapsed: true}, 6..7],
# nest
Expand All @@ -145,7 +150,55 @@ 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.set_col_width("A", 10)
|> Sheet.set_col_width("B", 10)
|> Sheet.set_col_width("C", 10)
|> Sheet.set_col_width("D", 10)
|> Sheet.set_col_width("E", 10)
|> Sheet.set_row_height(1, 75)
|> Sheet.set_row_height(2, 75)
|> Sheet.set_row_height(3, 75)
|> Sheet.set_row_height(4, 75)
|> Sheet.set_row_height(5, 75)
|> Sheet.insert_image(1, 1, "ladybug-3475779_640.jpg", width: 50, height: 50, align_x: :right, char: 9)
|> Sheet.insert_image(2, 2, "ladybug-3475779_640.jpg", width: 100, height: 100)
|> Sheet.insert_image(3, 0, {"ladybug-3475779_640.jpg", File.read!("ladybug-3475779_640.jpg")}, width: 150, height: 150)

sheet8 = %Sheet{
name: "Images 2",
rows: List.duplicate(["A", "B", "C", "D", "E"], 5)
}

sheet8 =
sheet8
|> Sheet.set_col_width("A", 10)
|> Sheet.set_col_width("B", 10)
|> Sheet.set_col_width("C", 10)
|> Sheet.set_col_width("D", 10)
|> Sheet.set_col_width("E", 10)
|> Sheet.set_row_height(1, 75)
|> Sheet.set_row_height(2, 75)
|> Sheet.set_row_height(3, 75)
|> Sheet.set_row_height(4, 75)
|> Sheet.set_row_height(5, 75)
|> Sheet.insert_image(0, 0, "ladybug-3475779_640.jpg",
width: 100,
height: 100,
x_offset: 50,
y_offset: 50
)

Workbook.append_sheet(workbook, sheet4)
|> Workbook.append_sheet(sheet5)
|> Workbook.append_sheet(sheet6)
|> Workbook.append_sheet(sheet7)
|> Workbook.append_sheet(sheet8)
|> Elixlsx.write_to("example.xlsx")
Binary file added ladybug-3475779_640.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 37 additions & 1 deletion lib/elixlsx/compiler.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
defmodule Elixlsx.Compiler do
alias Elixlsx.Compiler.WorkbookCompInfo
alias Elixlsx.Compiler.SheetCompInfo
alias Elixlsx.Compiler.DrawingCompInfo
alias Elixlsx.Compiler.CellStyleDB
alias Elixlsx.Compiler.DrawingDB
alias Elixlsx.Compiler.StringDB
alias Elixlsx.XML
alias Elixlsx.Sheet
Expand All @@ -25,6 +27,26 @@ defmodule Elixlsx.Compiler do
{Enum.reverse(sheetCompInfos), nextrID}
end

@doc """
Turn a list of %Sheet{} into a list of %DrawingCompInfo{}
and return the next rId after.
"""
@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
{dcis, _, next_id} =
Enum.reduce(sheets, {[], 1, init_rId}, fn acc, {dci, idx, rId} ->
if acc.images == [] do
{dci, idx, rId}
else
new_dci = DrawingCompInfo.make(idx, rId)
{dci ++ [new_dci], idx + 1, rId + 1}
end
end)

{dcis, next_id}
end

def compinfo_cell_pass_value(wci, value) do
cond do
is_binary(value) && XML.valid?(value) ->
Expand Down Expand Up @@ -59,6 +81,16 @@ 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 ->
Expand All @@ -71,16 +103,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)
Expand Down
27 changes: 27 additions & 0 deletions lib/elixlsx/compiler/drawing_comp_info.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
defmodule Elixlsx.Compiler.DrawingCompInfo do
alias __MODULE__

@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
49 changes: 49 additions & 0 deletions lib/elixlsx/compiler/drawing_db.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
defmodule Elixlsx.Compiler.DrawingDB do
alias __MODULE__
alias Elixlsx.Image

@moduledoc ~S"""
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.
"""

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 image_types(db) do
db.images
|> Enum.map(fn {i, _} -> {i.extension, i.type} end)
|> Enum.uniq()
end
end
6 changes: 5 additions & 1 deletion lib/elixlsx/compiler/workbook_comp_info.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
77 changes: 77 additions & 0 deletions lib/elixlsx/image.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
defmodule Elixlsx.Image do
alias Elixlsx.Image

@moduledoc ~S"""
An Image can either by a path to an image, or
a binary {"path_or_unique_id", <<binary>>}
char is the max character width of a font, this is
used when calculating how many pixels are in a column.
You might need to experiment with this value
depending on what font and size you are using.
"""

defstruct file_path: "",
type: "image/png",
extension: "png",
x: 0,
y: 0,
x_offset: 0,
y_offset: 0,
width: 1,
height: 1,
binary: nil,
align_x: :left,
char: 7

@type t :: %Image{
file_path: String.t() | {String.t(), binary},
type: String.t(),
extension: String.t(),
x: integer,
y: integer,
x_offset: integer,
y_offset: integer,
width: integer,
height: integer,
binary: binary | nil,
align_x: :left | :right,
char: integer
}

@doc """
Create an image struct based on opts
"""
def new(_, _, _, opts \\ [])

def new(file_path, x, y, opts) when is_binary(file_path) do
new({file_path, nil}, x, y, opts)
end

def new({file_path, binary}, x, y, opts) do
{ext, type} = image_type(file_path)

%Image{
file_path: file_path,
binary: binary,
type: type,
extension: ext,
x: x,
y: y,
x_offset: Keyword.get(opts, :x_offset, 0),
y_offset: Keyword.get(opts, :y_offset, 0),
width: Keyword.get(opts, :width, 1),
height: Keyword.get(opts, :height, 1),
align_x: Keyword.get(opts, :align_x, :left),
char: Keyword.get(opts, :char, 7)
}
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
Loading

0 comments on commit b9f47bc

Please sign in to comment.