• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

camatcode / basenji / 95ca0a8006ff931c34a58868f446a63fdb1c170d

08 Jul 2025 06:14PM UTC coverage: 69.083% (+1.5%) from 67.551%
95ca0a8006ff931c34a58868f446a63fdb1c170d

push

github

web-flow
Merge pull request #29 from camatcode/work/pdf-support

feat(comics): add pdf support

33 of 35 new or added lines in 1 file covered. (94.29%)

467 of 676 relevant lines covered (69.08%)

461.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.29
/lib/basenji/reader/pdf_reader.ex
1
defmodule Basenji.Reader.PDFReader do
2
  @moduledoc false
3

4
  import Basenji.Reader
5

6
  def format, do: :pdf
3✔
7

8
  def get_magic_numbers, do: [%{offset: 0, magic: [0x25, 0x50, 0x44, 0x46, 0x2D]}]
485✔
9

10
  def close(_any), do: :ok
4✔
11

12
  def get_entries(pdf_file_path, _opts \\ []) do
13
    with {:ok, %{pages: pages}} <- get_metadata(pdf_file_path) do
486✔
14
      padding = String.length("#{pages}") - 1
485✔
15

16
      file_entries =
485✔
17
        1..pages
18
        |> Enum.map(fn idx ->
19
          %{file_name: "#{String.pad_leading("#{idx}", padding, "0")}.jpeg"}
1,940✔
20
        end)
21

22
      {:ok, %{entries: file_entries}}
23
    end
24
  end
25

26
  def get_entry_stream!(pdf_file_path, entry) do
27
    file_name = entry[:file_name]
8✔
28
    {page_num, _rest} = Integer.parse(file_name)
8✔
29

30
    create_resource(fn ->
8✔
31
      with {:ok, output} <- exec("pdftoppm", ["-f", "#{page_num}", "-singlefile", "-jpeg", "-q", pdf_file_path]) do
8✔
32
        [output |> :binary.bin_to_list()]
33
      end
34
    end)
35
  end
36

37
  def read(pdf_file_path, _opts \\ []) do
38
    with {:ok, %{entries: file_entries}} <- get_entries(pdf_file_path) do
484✔
39
      file_entries =
484✔
40
        file_entries
41
        |> Enum.map(fn entry ->
42
          entry
43
          |> Map.put(:stream_fun, fn -> get_entry_stream!(pdf_file_path, entry) end)
1,936✔
44
        end)
45

46
      {:ok, %{entries: file_entries}}
47
    end
48
  end
49

50
  def get_metadata(pdf_file_path) do
51
    with {:ok, output} <- exec("pdfinfo", ["-isodates", pdf_file_path]) do
486✔
52
      metadata =
485✔
53
        String.split(output, "\n")
54
        |> Map.new(fn line ->
55
          [k, v] = String.split(line, ":", parts: 2)
8,730✔
56
          to_metadata(k, v)
8,730✔
57
        end)
58

59
      {:ok, metadata}
60
    end
61
  end
62

63
  defp to_metadata(k, v) do
64
    k = k |> String.trim() |> ProperCase.snake_case() |> String.to_atom()
8,730✔
65
    v = convert_value(k, v |> String.trim())
8,730✔
66
    {k, v}
67
  end
68

69
  defp convert_value(:creation_date, v), do: NaiveDateTime.from_iso8601!(v)
485✔
70
  defp convert_value(:mod_date, v), do: NaiveDateTime.from_iso8601!(v)
485✔
71
  defp convert_value(:pages, v), do: String.to_integer(v)
485✔
72

73
  defp convert_value(:filesize, v) do
74
    {first, _rest} = Integer.parse(v)
485✔
75
    first
485✔
76
  end
77

78
  defp convert_value(:pagesize, v) do
79
    String.split(v, " ")
80
    |> Enum.reduce([], fn part, acc ->
81
      Integer.parse(part)
82
      |> case do
1,940✔
83
        {coord, _rest} -> [coord | acc]
970✔
84
        _ -> acc
970✔
85
      end
86
    end)
87
    |> case do
485✔
88
      [x, y] -> {x, y}
485✔
NEW
89
      _ -> v
×
90
    end
91
  end
92

NEW
93
  defp convert_value(_k, "yes"), do: true
×
94
  defp convert_value(_k, "no"), do: false
3,880✔
95
  defp convert_value(_k, "none"), do: nil
485✔
96
  defp convert_value(_k, v), do: v
1,940✔
97
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc