foliokit 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ module Foliokit
2
+ module Overlay
3
+ class OverlayButton < OverlayBase
4
+ element_base
5
+ element_reader :source, selector: "data > overlayAsset", text: true
6
+
7
+ def tagname
8
+ "a"
9
+ end
10
+
11
+ def export(document, options = {})
12
+ node = super(document, options)
13
+ if source.present?
14
+ src = package.apply_asset(node, "source", source)
15
+ node["style"] ||= ""
16
+ node["style"] = [node["style"], "background-image: url(#{src})"].join(";")
17
+ end
18
+ node["swipable"] = true
19
+ node["touchable"] = true
20
+ node
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,10 @@
1
+ module Foliokit
2
+ module Overlay
3
+ class OverlayContainer < OverlayBase
4
+ element_base
5
+ def tagname
6
+ "div"
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,87 @@
1
+ module Foliokit
2
+ module Overlay
3
+ class OverlayHyperlink < OverlayBase
4
+ element_base
5
+ element_reader :url, selector: "data > url", text: true
6
+ element_reader :source, selector: "data > upStateURL", text: true
7
+ element_reader :active_source, selector: "data > downStateURL", text: true
8
+ element_reader :req_nav_confirm, type: "bool", selector: "data > reqNavConfirm", text: true
9
+ element_reader :open_in_app, type: "bool", selector: "data > openInApp", text: true
10
+
11
+ def tagname
12
+ "a"
13
+ end
14
+
15
+ def export(document, options = {})
16
+ node = super(document, options)
17
+ href = parse_url(node)
18
+ return nil unless href
19
+
20
+ node["href"] = href
21
+ node["confirm"] = req_nav_confirm
22
+ node["external"] = !open_in_app
23
+ node["swipable"] = true
24
+ node["touchable"] = true
25
+
26
+ if source.present?
27
+ img = Nokogiri::XML::Node.new("img", document)
28
+ package.apply_asset(img, "src", source)
29
+ img["class"] = "source"
30
+ node << img
31
+ end
32
+
33
+ if active_source.present?
34
+ img = Nokogiri::XML::Node.new("img", document)
35
+ package.apply_asset(img, "src", active_source)
36
+ img["class"] = "active-source"
37
+ node << img
38
+ end
39
+
40
+ node
41
+ end
42
+
43
+ def parse_url(node)
44
+ # history navitation
45
+ return url if url =~ %r{^goto://}
46
+
47
+ # url navigation
48
+ if url =~ %r{^http://.+} || url =~ %r{^https://.+}
49
+ uri = Addressable::URI.parse(url)
50
+ if open_in_app
51
+ params = URI.decode_www_form(uri.normalized_query || "").to_h
52
+ params["referrer"] = "Baker"
53
+ uri.query = URI.encode_www_form(params)
54
+ return uri.to_s
55
+ else
56
+ node["target"] = "_blank"
57
+ return url
58
+ end
59
+ elsif url =~ /^mailto:/
60
+ return url
61
+ end
62
+
63
+ # page / section navitation
64
+ if (matches = url.match(%r{^navto://relative/current#([0-9.]+)?}))
65
+ page_index = matches[1].to_i
66
+ return "#/#{page_index}/0"
67
+ elsif (matches = url.match(%r{^navto://tmp#?([0-9.]+)?}))
68
+ page_index = matches[1].to_i
69
+ return "#/#{page_index}/0"
70
+ elsif (matches = url.match(%r{^navto://([^#]+)#?([0-9.]+)?}))
71
+ page_id = matches[1]
72
+ page_index = @package.root_package.manifest.content_stack_ids.index(page_id)
73
+ return false if page_index.nil?
74
+
75
+ section_index = matches[2].to_i || 0
76
+ current_page_index = @package.root_package.manifest.content_stack_ids.index(@package.id)
77
+ current_section_index = @section_index
78
+ return false if page_index == current_page_index and section_index == current_section_index
79
+
80
+ return "#/#{page_index}/#{section_index}"
81
+ end
82
+
83
+ false
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,18 @@
1
+ module Foliokit
2
+ module Overlay
3
+ class OverlayImage < OverlayBase
4
+ element_base
5
+ element_reader :source, selector: "data > overlayAsset", text: true
6
+
7
+ def tagname
8
+ "img"
9
+ end
10
+
11
+ def export(document, options = {})
12
+ node = super(document, options)
13
+ package.apply_asset(node, "src", source, "div", self.bounds.width)
14
+ node
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,36 @@
1
+ module Foliokit
2
+ module Overlay
3
+ class OverlayImagepan < OverlayBase
4
+ element_base
5
+ element_reader :source, selector: "data > overlayAsset", text: true
6
+ element_reader :source_width, type: :i, selector: "data > overlayAsset", attribute: "width"
7
+ element_reader :source_height, type: :i, selector: "data > overlayAsset", attribute: "height"
8
+ element_reader :initial_viewport, type: Types::Bounds, selector: ->(options) { "data > initialViewport > #{package.manifest.orientation}Bounds > rectangle" }
9
+
10
+ def tagname
11
+ "div"
12
+ end
13
+
14
+ def export(document, options = {})
15
+ node = super(document, options)
16
+ if source.present?
17
+ src = package.apply_asset(node, "source", source)
18
+ node["style"] ||= ""
19
+ node["style"] = [node["style"], "background-image: url(#{src})"].join(";")
20
+ end
21
+
22
+ if initial_viewport.present?
23
+ zoom = source_width.to_f / initial_viewport.width.to_f
24
+ width = bounds.width * zoom
25
+ node["style"] ||= ""
26
+ node["style"] = [node["style"], "background-size: #{width}px"].join(";")
27
+ node["source-width"] = source_width
28
+ node["source-height"] = source_height
29
+ end
30
+
31
+ node["touchable"] = true
32
+ node
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,42 @@
1
+ module Foliokit
2
+ module Overlay
3
+ class OverlayVideo < OverlayBase
4
+ element_base
5
+ element_reader :videoUrl, selector: "data > videoUrl", text: true
6
+ element_reader :playInContext, type: "bool", selector: "data > playInContext", text: true
7
+ element_reader :showControlsByDefault, type: "bool", selector: "data > showControlsByDefault", text: true
8
+ element_reader :allowPause, type: "bool", selector: "data > allowPause", text: true
9
+ element_reader :hideWhenFinishedPlaying, type: "bool", selector: "data > hideWhenFinishedPlaying", text: true
10
+ element_reader :autoStart, type: "bool", selector: "data > autoStart", text: true
11
+ element_reader :autoStartDelay, type: "f", selector: "data > autoStartDelay", text: true
12
+
13
+ def tagname
14
+ "div"
15
+ end
16
+
17
+ def export(document, options = {})
18
+ node = super(document, options)
19
+
20
+ # process youtube videos
21
+ uri = Addressable::URI.parse(video_url)
22
+ if uri.absolute? and uri.host =~ /(youtube)/
23
+ node["touchable"] = true
24
+ node["data-type"] = "youtube"
25
+ node["data-src"] = uri.to_s
26
+ node["class"] = "overlay-webview"
27
+ return node
28
+ end
29
+
30
+ node["src"] = package.store_asset(video_url)
31
+ node["auto-start"] = auto_start
32
+ node["loop"] = hide_when_finished_playing
33
+ node["fullscreen"] = play_in_context ? false : true
34
+ node["playsinline"] = "playsinline" if play_in_context
35
+ node["controls"] = true if show_controls_by_default
36
+ node["swipable"] = true
37
+ node["touchable"] = true
38
+ node
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,57 @@
1
+ module Foliokit
2
+ module Overlay
3
+ class OverlayWebview < OverlayBase
4
+ element_base
5
+ element_reader :web_view_url, selector: "data > webViewUrl", text: true
6
+
7
+ def tagname
8
+ "div"
9
+ end
10
+
11
+ def export(document, options = {})
12
+ node = super(document, options)
13
+ node["touchable"] = true
14
+ node["data-type"] = "iframe"
15
+ uri = Addressable::URI.parse(web_view_url)
16
+
17
+ # Handle simple iframe embeds
18
+ if uri.relative?
19
+ file = package.dir.file(uri.to_s)
20
+ return nil unless file.exists?
21
+
22
+ elements = Nokogiri::HTML(file.read).css("body > *")
23
+ if elements.count == 1 and elements.first.name == "iframe"
24
+ uri = Addressable::URI.parse(elements.first["src"])
25
+ uri.query = "referrer=WebView"
26
+ end
27
+ end
28
+
29
+ # Process embed type
30
+ if uri.absolute?
31
+ if uri.host =~ /(youtube)/
32
+ node["data-type"] = "youtube"
33
+ end
34
+ else
35
+ file = package.dir.file(uri.to_s)
36
+ return nil unless file.exists?
37
+
38
+ html_dir_path = file.dir.relative_path(package.dir)
39
+ html_dir_hash = Digest::MD5.hexdigest(html_dir_path)
40
+ html_dir = package.root.dir("html").dir(html_dir_hash)
41
+ unless html_dir.exists?
42
+ puts "-- creating html dir"
43
+ file.dir.copy(html_dir)
44
+ end
45
+ html_file = html_dir.file(file.name)
46
+ uri = Addressable::URI.parse(html_file.relative_path(package.root))
47
+ uri.query = "referrer=WebView"
48
+ end
49
+
50
+ # Set source
51
+ node["data-src"] = uri.to_s
52
+
53
+ node
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,123 @@
1
+ module Foliokit
2
+ class Package
3
+ attr_reader :file, :dir, :manifest, :root
4
+ attr_accessor :assets, :root_package
5
+
6
+ def initialize(file, dir, root, root_package = nil)
7
+ @file = file
8
+ @dir = dir
9
+ @root = root
10
+ @root_package = root_package
11
+ @dir.delete if @dir.exists? and !@dir.file("Folio.xml").exists?
12
+ file.extract(@dir) unless @dir.exists?
13
+ unwrap_folder! if @dir.files.none?
14
+ wrap_article! if root_package.nil? and @dir.file("Article.xml").exists?
15
+ if @dir.file("Folio.xml").exists?
16
+ @manifest = Foliokit::ManifestFolio.new(@dir.file("Folio.xml"), self)
17
+ elsif @dir.file("Article.xml").exists?
18
+ @manifest = Foliokit::ManifestArticle.new(@dir.file("Article.xml"), self)
19
+ else
20
+ raise "invalid folio format"
21
+ end
22
+ @assets = {}
23
+ @checksums = {}
24
+ end
25
+
26
+ def id
27
+ manifest.id
28
+ end
29
+
30
+ def content_stacks
31
+ manifest.content_stacks
32
+ end
33
+
34
+ def valid_overlay_id?(id)
35
+ manifest.valid_overlay_id?(id)
36
+ end
37
+
38
+ def subfolios(root_package)
39
+ manifest.subfolios.map do |element|
40
+ Foliokit::Package.new(dir.file(element[:subfolio]), dir.dir(element[:id]), root, root_package)
41
+ end
42
+ end
43
+
44
+ def apply_asset(node, attribute, path, fallback_tag = nil, target_width = nil)
45
+ src = store_asset(path, target_width)
46
+ if src.nil?
47
+ node.name = fallback_tag if fallback_tag.present?
48
+ else
49
+ node[attribute] = src
50
+ end
51
+ end
52
+
53
+ def store_asset(path, target_width = nil, bounds = nil)
54
+ parts = path.split("#")
55
+ source = parts[0]
56
+ index = (parts[1]&.to_i or 1) - 1
57
+ source_file = dir.file(source)
58
+ source_file.align_encoding! unless source_file.exists?
59
+ return "#" unless source_file.exists?
60
+
61
+ hash = Digest::MD5.hexdigest("#{dir.name}/#{source}/#{index}/#{bounds}")
62
+ extension = source_file.extension
63
+ asset_file = root.file("assets/#{hash}.#{extension}")
64
+ asset_file.dir.create unless asset_file.dir.exists?
65
+ checksum = "#{Digest::SHA256.file(source_file.to_s)}/#{index}/#{bounds}"
66
+ return @checksums[checksum] if @checksums.key?(checksum)
67
+
68
+ # generate image from pdf or copy file
69
+ if source_file.mimetype == "application/pdf"
70
+ # Convert PDF images
71
+ doc = Poppler::Document.new(source_file.to_s)
72
+ page = doc[index]
73
+ surface = Cairo::ImageSurface.new(Cairo::FORMAT_ARGB32, *page.size)
74
+ cr = Cairo::Context.new(surface)
75
+ cr.render_poppler_page(page)
76
+ asset_file = root.file("assets/#{hash}.png")
77
+ cr.target.write_to_png(asset_file.to_s)
78
+ cr.target.finish
79
+ else
80
+ source_file.copy(asset_file)
81
+ end
82
+
83
+ # Crop to bounds
84
+ asset_file.crop(bounds.to_h) unless bounds.nil?
85
+
86
+ # Optimize image
87
+ asset_file.optimize!
88
+
89
+ # Cache and return result
90
+ @checksums[checksum] = asset_file.relative_path(root)
91
+ @checksums[checksum]
92
+ end
93
+
94
+ private
95
+
96
+ def wrap_article!
97
+ article = Nokogiri::XML(dir.file("Article.xml").read).css("article").first
98
+ dir.clean
99
+ file.copy(dir.file(file.name))
100
+ xml = Nokogiri::XML::Builder.new do |b|
101
+ b.folio(version: article["version"], lastUpdated: article["lastUpdated"], date: article["lastUpdated"], orientation: article["orientation"], bindingDirection: "left", id: file.basename, targetViewer: article["targetViewer"], article: "true") do
102
+ b.metadata do
103
+ b.description file.basename.titleize
104
+ b.magazineTitle file.basename.titleize
105
+ b.folioNumber file.basename.titleize
106
+ end
107
+ b.targetDimensions do
108
+ b.targetDimension(wideDimension: [article["width"].to_i, article["height"].to_i].max, narrowDimension: [article["width"].to_i, article["height"].to_i].min)
109
+ end
110
+ b.contentStacks do
111
+ b.contentStack(id: file.basename, subfolio: file.name)
112
+ end
113
+ end
114
+ end
115
+ dir.file("Folio.xml").write(xml.doc.to_s)
116
+ end
117
+
118
+ def unwrap_folder!
119
+ dirs = dir.directories.reject { |d| (/^_/ =~ d.name) }
120
+ @dir = dirs.first if dirs.count == 1
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,9 @@
1
+ module Foliokit
2
+ class Slide
3
+ attr_reader :articles
4
+
5
+ def initialize(articles = [])
6
+ @articles = articles
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ module Foliokit
2
+ class State
3
+ include Modules::Element
4
+ element_reader :id, hash: true
5
+ end
6
+ end
@@ -0,0 +1,47 @@
1
+ module Foliokit
2
+ module Types
3
+ class Bounds
4
+ attr_reader :x, :y, :width, :height
5
+
6
+ def self.convert(target, name, options = {})
7
+ self.new(target[:x].to_i, target[:y].to_i, target[:width].to_i, target[:height].to_i)
8
+ end
9
+
10
+ def initialize(x, y, width, height)
11
+ @x = x
12
+ @y = y
13
+ @width = width
14
+ @height = height
15
+ end
16
+
17
+ def translate(x = 0, y = 0)
18
+ @x += x
19
+ @y += y
20
+ end
21
+
22
+ def top
23
+ @y
24
+ end
25
+
26
+ def left
27
+ @x
28
+ end
29
+
30
+ def bottom
31
+ @y + @height
32
+ end
33
+
34
+ def right
35
+ @x + @width
36
+ end
37
+
38
+ def to_s
39
+ "#{x}-#{y}-#{width}-#{height}"
40
+ end
41
+
42
+ def to_h
43
+ { x: x, y: y, width: width, height: height }
44
+ end
45
+ end
46
+ end
47
+ end