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

camatcode / basenji / ec5591dff434575811698a14a3e9c658250d85ea

08 Jul 2025 06:07PM UTC coverage: 69.083% (+1.5%) from 67.551%
ec5591dff434575811698a14a3e9c658250d85ea

Pull #29

github

camatcode
feat(comics): add pdf support
Pull Request #29: 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%)

481.31 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
4✔
7

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

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

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

16
      file_entries =
502✔
17
        1..pages
18
        |> Enum.map(fn idx ->
19
          %{file_name: "#{String.pad_leading("#{idx}", padding, "0")}.jpeg"}
2,008✔
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]
9✔
28
    {page_num, _rest} = Integer.parse(file_name)
9✔
29

30
    create_resource(fn ->
9✔
31
      with {:ok, output} <- exec("pdftoppm", ["-f", "#{page_num}", "-singlefile", "-jpeg", "-q", pdf_file_path]) do
9✔
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
501✔
39
      file_entries =
501✔
40
        file_entries
41
        |> Enum.map(fn entry ->
42
          entry
43
          |> Map.put(:stream_fun, fn -> get_entry_stream!(pdf_file_path, entry) end)
2,004✔
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
503✔
52
      metadata =
502✔
53
        String.split(output, "\n")
54
        |> Map.new(fn line ->
55
          [k, v] = String.split(line, ":", parts: 2)
9,036✔
56
          to_metadata(k, v)
9,036✔
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()
9,036✔
65
    v = convert_value(k, v |> String.trim())
9,036✔
66
    {k, v}
67
  end
68

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

73
  defp convert_value(:filesize, v) do
74
    {first, _rest} = Integer.parse(v)
502✔
75
    first
502✔
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
2,008✔
83
        {coord, _rest} -> [coord | acc]
1,004✔
84
        _ -> acc
1,004✔
85
      end
86
    end)
87
    |> case do
502✔
88
      [x, y] -> {x, y}
502✔
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
4,016✔
95
  defp convert_value(_k, "none"), do: nil
502✔
96
  defp convert_value(_k, v), do: v
2,008✔
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