Skip to content

Commit 62e3e7c

Browse files
committed
Aspect ratio-preserving image scale
1 parent ed84001 commit 62e3e7c

4 files changed

Lines changed: 103 additions & 6 deletions

File tree

lib/mudbrick.ex

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ defmodule Mudbrick do
134134
## Options
135135
136136
- `:position` - `{x, y}` in points, relative to bottom-left corner.
137-
- `:scale` - `{w, h}` in points.
137+
- `:scale` - `{w, h}` in points. To preserve aspect ratio, set either, but not both, to `:auto`.
138138
- `:skew` - `{x, y}`, passed through to PDF `cm` operator.
139139
140140
All options default to `{0, 0}`.
@@ -145,24 +145,43 @@ defmodule Mudbrick do
145145
...> |> Mudbrick.page()
146146
...> |> Mudbrick.image(:lovely_flower, position: {100, 100}, scale: {100, 100})
147147
148+
Forgetting to register the image:
149+
148150
iex> Mudbrick.new()
149151
...> |> Mudbrick.page()
150152
...> |> Mudbrick.image(:my_face, position: {100, 100}, scale: {100, 100})
151153
** (Mudbrick.Image.Unregistered) Unregistered image: my_face
152154
155+
Auto height:
156+
157+
iex> Mudbrick.new(images: %{lovely_flower: [file: Mudbrick.TestHelper.flower()]})
158+
...> |> Mudbrick.page(size: {50, 50})
159+
...> |> Mudbrick.image(:lovely_flower, position: {0, 0}, scale: {50, :auto})
160+
...> |> Mudbrick.render()
161+
...> |> then(&File.write("examples/image_auto_aspect_scale.pdf", &1))
162+
163+
<object width="400" height="100" data="examples/image_auto_aspect_scale.pdf?#navpanes=0" type="application/pdf"></object>
164+
165+
Attempting to set both width and height to `:auto`:
166+
167+
iex> Mudbrick.new(images: %{lovely_flower: [file: Mudbrick.TestHelper.flower()]})
168+
...> |> Mudbrick.page()
169+
...> |> Mudbrick.image(:lovely_flower, position: {100, 100}, scale: {:auto, :auto})
170+
** (Mudbrick.Image.AutoScalingError) Auto scaling works with width or height, but not both.
171+
153172
Tip: to make the image fit the page, pass e.g. `Page.size(:a4)` as the
154173
`scale` and `{0, 0}` as the `position`.
155174
"""
156175

157-
@spec image(context(), atom(), ContentStream.Cm.options()) :: context()
176+
@spec image(context(), atom(), Image.image_options()) :: context()
158177
def image({doc, _content_stream_obj} = context, user_identifier, opts \\ []) do
159178
import ContentStream
160179

161180
case Map.fetch(Document.root_page_tree(doc).value.images, user_identifier) do
162181
{:ok, image} ->
163182
context
164183
|> add(%ContentStream.QPush{})
165-
|> add(ContentStream.Cm.new(opts))
184+
|> add(ContentStream.Cm.new(cm_opts(image.value, opts)))
166185
|> add(%ContentStream.Do{image: image.value})
167186
|> add(%ContentStream.QPop{})
168187

@@ -282,6 +301,29 @@ defmodule Mudbrick do
282301
end)
283302
end
284303

304+
@spec cm_opts(Mudbrick.Image.t(), Image.image_options()) :: Mudbrick.ContentStream.Cm.options()
305+
defp cm_opts(image, image_opts) do
306+
scale =
307+
case image_opts[:scale] do
308+
{:auto, :auto} ->
309+
raise Mudbrick.Image.AutoScalingError,
310+
"Auto scaling works with width or height, but not both."
311+
312+
{w, :auto} ->
313+
ratio = w / image.width
314+
{w, image.height * ratio}
315+
316+
{:auto, h} ->
317+
ratio = h / image.height
318+
{image.width * ratio, h}
319+
320+
otherwise ->
321+
otherwise
322+
end
323+
324+
Keyword.put(image_opts, :scale, scale)
325+
end
326+
285327
defp fetch_font(doc, opts) do
286328
Keyword.update(opts, :font, nil, fn user_identifier ->
287329
case Map.fetch(Document.root_page_tree(doc).value.fonts, user_identifier) do

lib/mudbrick/content_stream/cm.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ defmodule Mudbrick.ContentStream.Cm do
44
| {:scale, Mudbrick.coords()}
55
| {:skew, Mudbrick.coords()}
66

7-
@type options :: [options()]
7+
@type options :: [option()]
88

99
@type t :: %__MODULE__{
1010
position: Mudbrick.coords(),

lib/mudbrick/image.ex

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
defmodule Mudbrick.Image do
2-
@moduledoc false
2+
@type t :: %__MODULE__{
3+
file: iodata(),
4+
resource_identifier: atom(),
5+
width: number(),
6+
height: number(),
7+
bits_per_component: number(),
8+
filter: :DCTDecode
9+
}
10+
11+
@type scale_dimension :: number() | :auto
12+
@type scale :: {scale_dimension(), scale_dimension()}
13+
@type image_option ::
14+
{:position, Mudbrick.coords()}
15+
| {:scale, scale()}
16+
| {:skew, Mudbrick.coords()}
17+
@type image_options :: [image_option()]
318

419
@enforce_keys [:file, :resource_identifier]
520
defstruct [
@@ -8,10 +23,13 @@ defmodule Mudbrick.Image do
823
:width,
924
:height,
1025
:bits_per_component,
11-
:colour_space,
1226
:filter
1327
]
1428

29+
defmodule AutoScalingError do
30+
defexception [:message]
31+
end
32+
1533
defmodule Unregistered do
1634
defexception [:message]
1735
end
@@ -23,6 +41,8 @@ defmodule Mudbrick.Image do
2341
alias Mudbrick.Document
2442
alias Mudbrick.Stream
2543

44+
@doc false
45+
@spec new(Keyword.t()) :: t()
2646
def new(opts) do
2747
struct!(
2848
__MODULE__,
@@ -33,6 +53,7 @@ defmodule Mudbrick.Image do
3353
)
3454
end
3555

56+
@doc false
3657
def add_objects(doc, images) do
3758
{doc, image_objects, _id} =
3859
for {human_name, image_opts} <- images, reduce: {doc, %{}, 0} do

test/image_test.exs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,40 @@ defmodule Mudbrick.ImageTest do
3030
end
3131
end
3232

33+
test "specifying :auto height maintains aspect ratio" do
34+
assert [
35+
"q",
36+
"100 0 0 95.4 123 456 cm",
37+
"/I1 Do",
38+
"Q"
39+
] =
40+
new(images: %{flower: [file: flower()]})
41+
|> page()
42+
|> image(
43+
:flower,
44+
position: {123, 456},
45+
scale: {100, :auto}
46+
)
47+
|> operations()
48+
end
49+
50+
test "specifying :auto width maintains aspect ratio" do
51+
assert [
52+
"q",
53+
"52.41090146750524 0 0 50 123 456 cm",
54+
"/I1 Do",
55+
"Q"
56+
] =
57+
new(images: %{flower: [file: flower()]})
58+
|> page()
59+
|> image(
60+
:flower,
61+
position: {123, 456},
62+
scale: {:auto, 50}
63+
)
64+
|> operations()
65+
end
66+
3367
test "asking for a registered image produces an isolated cm/Do operation" do
3468
assert [
3569
"q",

0 commit comments

Comments
 (0)