Skip to content

Commit

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

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

I have 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 c2c2fd5
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 c2c2fd5

Please sign in to comment.