Class: Okab::Document

Inherits:
Object
  • Object
show all
Defined in:
lib/okab/document.rb,
sig/okab.rbs

Instance Method Summary collapse

Constructor Details

#initialize(title: nil, author: nil, creator: "Okab") ⇒ Document

Returns a new instance of Document.

Parameters:

  • title: (String, nil) (defaults to: nil)
  • author: (String, nil) (defaults to: nil)
  • creator: (String, nil) (defaults to: "Okab")


7
8
9
10
# File 'lib/okab/document.rb', line 7

def initialize(title: nil, author: nil, creator: "Okab")
  @metadata = {Title: title || "", Author: author || "", Creator: creator}
  @pages, @fonts, @images, @opacities, @outlines = [], {}, {}, {}, []
end

Instance Method Details

#embed_font(font, subset: true) ⇒ EmbeddedFont

Parameters:

  • font (Font, Alhena::Font)
  • subset: (Boolean) (defaults to: true)

Returns:

Raises:

  • (ArgumentError)


32
33
34
35
36
37
38
# File 'lib/okab/document.rb', line 32

def embed_font(font, subset: true)
  font = Font.new(font) if font.is_a?(Alhena::Font)
  raise ArgumentError, "font must be an Okab::Font or Alhena::Font" unless font.is_a?(Font)

  key = [font.face.object_id, !!subset]
  @fonts[key] ||= EmbeddedFont.new(self, font, subset: subset)
end

#opacity_name(value) ⇒ String

Parameters:

  • value (Numeric)

Returns:

  • (String)

Raises:

  • (ArgumentError)


48
49
50
51
52
# File 'lib/okab/document.rb', line 48

def opacity_name(value)
  raise ArgumentError, "opacity must be finite and between 0 and 1" unless value.is_a?(Numeric) && !value.is_a?(Complex) && value.finite? && value.between?(0, 1)

  @opacities[value.to_f] ||= "GS#{@opacities.length + 1}"
end

#outline(title, page:, level: 0) ⇒ self

Parameters:

  • title (String)
  • page: (Page)
  • level: (Integer) (defaults to: 0)

Returns:

  • (self)

Raises:

  • (ArgumentError)


19
20
21
22
23
24
25
26
# File 'lib/okab/document.rb', line 19

def outline(title, page:, level: 0)
  raise ArgumentError, "outline page belongs to another document" unless @pages.include?(page)
  raise ArgumentError, "outline level must be a nonnegative integer" unless level.is_a?(Integer) && level >= 0
  raise ArgumentError, "outline level cannot skip a parent" if level > (@outlines.last&.last || -1) + 1

  @outlines << [String(title).dup.freeze, page, level]
  self
end

#owns_page?(page) ⇒ Boolean

Parameters:

Returns:

  • (Boolean)


28
29
30
# File 'lib/okab/document.rb', line 28

def owns_page?(page)
  @pages.include?(page)
end

#page(width:, height:) ⇒ Page #page(width:, height:) ⇒ Page

Overloads:

  • #page(width:, height:) ⇒ Page

    Parameters:

    • width: (Numeric)
    • height: (Numeric)

    Returns:

  • #page(width:, height:) ⇒ Page

    Parameters:

    • width: (Numeric)
    • height: (Numeric)

    Returns:

Yields:

Yield Parameters:

Yield Returns:

  • (void)


12
13
14
15
16
17
# File 'lib/okab/document.rb', line 12

def page(width:, height:)
  page = Page.new(self, width: width, height: height)
  @pages << page
  yield page if block_given?
  page
end

#register_image(image) ⇒ String

Parameters:

Returns:

  • (String)

Raises:

  • (ArgumentError)


40
41
42
43
44
45
46
# File 'lib/okab/document.rb', line 40

def register_image(image)
  raise ArgumentError, "expected an Okab::Image" unless image.is_a?(Image)

  key = image.object_id
  @images[key] ||= ["Im#{@images.length + 1}", image]
  @images.fetch(key).first
end

#render ⇒ String

Returns:

  • (String)

Raises:



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/okab/document.rb', line 54

def render
  raise InvalidDocument, "document has no pages" if @pages.empty?

  writer = PDF::Writer.new
  pages_ref, catalog_ref, info_ref = writer.reserve, writer.reserve, writer.reserve
  page_refs = @pages.map { writer.reserve }
  font_resources = render_fonts(writer)
  image_resources = render_images(writer)
  opacity_resources = @opacities.to_h do |opacity, name|
    [name, writer.add("<< /Type /ExtGState /ca #{number(opacity)} /CA #{number(opacity)} >>")]
  end

  @pages.zip(page_refs).each do |page, page_ref|
    font_names = font_resources.to_h { |key, resource| [key.object_id, resource[:name]] }
    content = page.content(fonts: font_names)
    content_ref = writer.stream(content)
    annotations = page.annotations.map { |annotation| annotation_object(writer, annotation, page_refs) }
    fonts = font_resources.values.to_h { |font| [font[:name], font[:ref]] }
    resources = "<< /Font #{resource_dictionary(fonts)} " \
      "/XObject #{resource_dictionary(image_resources)} " \
      "/ExtGState #{resource_dictionary(opacity_resources)} >>"
    box = "[0 0 #{number(page.width)} #{number(page.height)}]"
    annotation_refs = annotations.empty? ? "" : "/Annots [#{annotations.map { |ref| "#{ref} 0 R" }.join(' ')}]"
    writer.set(page_ref, "<< /Type /Page /Parent #{pages_ref} 0 R /MediaBox #{box} /Resources #{resources} /Contents #{content_ref} 0 R #{annotation_refs} >>")
  end

  writer.set(pages_ref, "<< /Type /Pages /Count #{@pages.length} /Kids [#{page_refs.map { |ref| "#{ref} 0 R" }.join(' ')}] >>")
  outline_root = render_outlines(writer, page_refs)
  catalog = "<< /Type /Catalog /Pages #{pages_ref} 0 R"
  catalog << " /Outlines #{outline_root} 0 R /PageMode /UseOutlines" if outline_root
  catalog << " >>"
  writer.set(catalog_ref, catalog)
  writer.set(info_ref, "<< #{@metadata.map { |key, value| "/#{key} #{pdf_text(value)}" }.join(' ')} >>")
  writer.render(root: catalog_ref, info: info_ref)
end

#write(path) ⇒ String

Parameters:

  • path (String)

Returns:

  • (String)


90
91
92
93
# File 'lib/okab/document.rb', line 90

def write(path)
  File.binwrite(path, render)
  path
end