lib/pdf/stream.ex

defmodule Pdf.Stream do
  @moduledoc false
  defstruct compress: 6, size: 0, dictionary: %{}, content: []

  import Pdf.Size
  import Pdf.Utils
  alias Pdf.Dictionary

  @stream_start "\nstream\n"
  @stream_end "endstream"

  def new(opts \\ []), do: init(opts, %__MODULE__{})

  defp init([], stream), do: stream

  defp init([{:compress, true} | tail], stream),
    do: init(tail, %{stream | compress: 6})

  defp init([{:compress, compress} | tail], stream),
    do: init(tail, %{stream | compress: compress})

  defp init([{:dictionary, dictionary} | tail], stream),
    do: init(tail, %{stream | dictionary: dictionary})

  defp init([_ | tail], stream),
    do: init(tail, stream)

  def push(stream, {:command, _} = command) do
    # +1 because we are adding a newline
    size = size_of(command) + 1
    %{stream | size: stream.size + size, content: ["\n", command | stream.content]}
  end

  def push(stream, command), do: push(stream, c(command))

  def set(stream, content) when is_binary(content) do
    size = size_of(content)
    %{stream | size: size, content: [content]}
  end

  def to_iolist(%{compress: false} = stream) do
    dictionary = Dictionary.new(Map.merge(stream.dictionary, %{"Length" => stream.size}))

    Pdf.Export.to_iolist([
      dictionary,
      @stream_start,
      Enum.reverse(stream.content),
      @stream_end
    ])
  end

  def to_iolist(%{compress: level} = stream) do
    compressed =
      stream.content
      |> Enum.reverse()
      |> Pdf.Export.to_iolist()
      |> compress(level)

    dictionary =
      Dictionary.new(
        Map.merge(stream.dictionary, %{
          "Length" => byte_size(compressed),
          "Filter" => n("FlateDecode")
        })
      )

    Pdf.Export.to_iolist([
      dictionary,
      @stream_start,
      compressed,
      @stream_end
    ])
  end

  defp compress(iodata, level) do
    z = :zlib.open()
    :zlib.deflateInit(z, level)
    compressed = :zlib.deflate(z, iodata, :finish)
    :zlib.deflateEnd(z)
    IO.iodata_to_binary(compressed)
  end

  defimpl Pdf.Export do
    def to_iolist(stream), do: Pdf.Stream.to_iolist(stream)
  end
end