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

camatcode / basenji / c0c3386ac4393a4f105fa48bf7a0a348a4e6caa6

12 Jul 2025 04:02PM UTC coverage: 75.056% (+1.4%) from 73.621%
c0c3386ac4393a4f105fa48bf7a0a348a4e6caa6

Pull #39

github

camatcode
feat: added comic optimizer that makes CBZs
Pull Request #39: WIP: chore: stashing

55 of 59 new or added lines in 5 files covered. (93.22%)

23 existing lines in 6 files now uncovered.

668 of 890 relevant lines covered (75.06%)

566.15 hits per line

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

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

4
  alias Basenji.Reader.CB7Reader
5
  alias Basenji.Reader.CBRReader
6
  alias Basenji.Reader.CBTReader
7
  alias Basenji.Reader.CBZReader
8
  alias Basenji.Reader.PDFReader
9
  alias Basenji.Reader.Process.JPEGOptimizer
10
  alias Basenji.Reader.Process.PNGOptimizer
11
  alias Porcelain.Result
12

13
  @readers [
14
    CBZReader,
15
    CBRReader,
16
    CB7Reader,
17
    CBTReader,
18
    PDFReader
19
  ]
20

21
  @optimizers [
22
    JPEGOptimizer,
23
    PNGOptimizer
24
  ]
25

26
  @image_extensions [
27
    "jpeg",
28
    "jpg",
29
    "jpe",
30
    "jif",
31
    "jfi",
32
    "jfif",
33
    "heif",
34
    "heic",
35
    "png",
36
    "gif",
37
    "svg",
38
    "eps",
39
    "webp",
40
    "tiff",
41
    "tif"
42
  ]
43

44
  def info(location, opts \\ []) do
45
    reader = find_reader(location)
55✔
46

47
    info =
55✔
48
      if reader do
2✔
49
        title = location |> Path.basename() |> Path.rootname()
53✔
50
        {:ok, response} = reader.read(location, opts)
53✔
51
        %{entries: entries} = response
53✔
52
        reader.close(response[:file])
53✔
53

54
        %{format: reader.format(), resource_location: location, title: title, page_count: Enum.count(entries)}
53✔
55
      else
56
        {:error, :unreadable}
57
      end
58

59
    info
60
    |> case do
55✔
61
      {:error, e} -> {:error, e}
2✔
62
      inf -> {:ok, inf}
53✔
63
    end
64
  end
65

66
  def exec(cmd, args, opts \\ []) do
67
    Porcelain.exec(cmd, args, opts)
68
    |> case do
2,148✔
69
      %Result{out: output, status: 0} ->
2,147✔
70
        {:ok, output |> String.trim()}
71

72
      other ->
1✔
73
        {:error, other}
74
    end
75
  end
76

77
  def create_resource(make_func) do
78
    Stream.resource(
190✔
79
      make_func,
80
      fn
81
        :halt -> {:halt, nil}
190✔
82
        func -> {func, :halt}
190✔
83
      end,
84
      fn _ -> nil end
190✔
85
    )
86
  end
87

88
  def sort_file_names(e), do: Enum.sort_by(e, & &1.file_name)
2,652✔
89

90
  def reject_macos_preview(e), do: Enum.reject(e, &String.contains?(&1.file_name, "__MACOSX"))
2,652✔
91

92
  def reject_directories(e), do: Enum.reject(e, &(Path.extname(&1.file_name) == ""))
2,652✔
93

94
  def reject_non_image(e) do
95
    Enum.filter(e, fn ent ->
2,652✔
96
      ext = Path.extname(ent.file_name) |> String.replace(".", "") |> String.downcase()
10,608✔
97
      ext in @image_extensions
10,608✔
98
    end)
99
  end
100

101
  def read(file_path, opts \\ []) do
102
    opts = Keyword.merge([optimize: false], opts)
3,297✔
103

104
    reader = find_reader(file_path)
3,297✔
105

106
    if reader do
3,297✔
107
      read_result = reader.read(file_path, opts)
3,297✔
108
      if opts[:optimize], do: optimize_entries(read_result), else: read_result
3,297✔
109
    else
110
      {:error, :no_reader_found}
111
    end
112
  end
113

114
  def find_reader(file_path) do
115
    @readers
116
    |> Enum.reduce_while(
3,352✔
117
      nil,
118
      fn reader, _acc ->
119
        if matches_extension?(reader, file_path) && matches_magic?(reader, file_path),
10,180✔
120
          do: {:halt, reader},
121
          else: {:cont, nil}
122
      end
123
    )
124
  end
125

126
  def stream_pages(file_path, opts \\ []) do
127
    opts = Keyword.merge([start_page: 1], opts)
17✔
128

129
    with {:ok, %{entries: entries}} <- read(file_path, opts) do
17✔
130
      stream =
17✔
131
        opts[:start_page]..Enum.count(entries)
132
        |> Stream.map(fn idx ->
133
          at = idx - 1
68✔
134
          Enum.at(entries, at).stream_fun.()
68✔
135
        end)
136

137
      {:ok, stream}
138
    end
139
  end
140

141
  def matches_extension?(reader, filepath) do
142
    file_ext = Path.extname(filepath) |> String.downcase()
10,180✔
143

144
    reader.file_extensions()
10,180✔
145
    |> Enum.reduce_while(false, fn ext, acc ->
10,180✔
146
      if String.ends_with?(file_ext, ext), do: {:halt, true}, else: {:cont, acc}
10,180✔
147
    end)
148
  end
149

150
  def matches_magic?(reader, file_path) do
151
    reader.get_magic_numbers()
3,350✔
152
    |> Enum.reduce_while(
3,350✔
153
      nil,
154
      fn %{offset: offset, magic: magic}, _acc ->
155
        try do
3,350✔
156
          bytes =
3,350✔
157
            File.stream!(file_path, offset + Enum.count(magic), [])
158
            |> Enum.take(1)
159
            |> hd()
160
            |> :binary.bin_to_list()
161
            |> Enum.take(-1 * Enum.count(magic))
162

163
          if bytes == magic, do: {:halt, true}, else: {:cont, nil}
3,350✔
164
        rescue
UNCOV
165
          _e ->
×
166
            {:cont, nil}
167
        end
168
      end
169
    )
170
  end
171

172
  defp optimize_entries({:ok, result}) do
173
    updated_entries =
48✔
174
      Map.get(result, :entries)
175
      |> Enum.map(fn entry ->
176
        stream_fun = fn ->
192✔
177
          create_resource(fn -> [optimize(entry.stream_fun.()) |> :binary.bin_to_list()] end)
75✔
178
        end
179

180
        Map.put(entry, :stream_fun, stream_fun)
192✔
181
      end)
182

183
    result = Map.put(result, :entries, updated_entries)
48✔
184

185
    {:ok, result}
186
  end
187

UNCOV
188
  defp optimize_entries(other), do: other
×
189

190
  defp optimize(bytes) do
191
    @optimizers
192
    |> Enum.reduce(bytes |> Enum.to_list(), fn reader, bytes ->
75✔
193
      reader.optimize!(bytes)
150✔
194
    end)
195
  end
196
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