wax_iiif 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +29 -0
- data/.rspec +2 -0
- data/.travis.yml +11 -0
- data/Gemfile +4 -0
- data/Guardfile +10 -0
- data/LICENSE.md +30 -0
- data/README.md +8 -0
- data/Rakefile +24 -0
- data/lib/iiif_s3/base_properties.rb +95 -0
- data/lib/iiif_s3/builder.rb +254 -0
- data/lib/iiif_s3/collection.rb +61 -0
- data/lib/iiif_s3/config.rb +142 -0
- data/lib/iiif_s3/errors.rb +37 -0
- data/lib/iiif_s3/full_image.rb +20 -0
- data/lib/iiif_s3/image_info.rb +96 -0
- data/lib/iiif_s3/image_record.rb +141 -0
- data/lib/iiif_s3/image_tile.rb +46 -0
- data/lib/iiif_s3/image_variant.rb +126 -0
- data/lib/iiif_s3/manifest.rb +151 -0
- data/lib/iiif_s3/thumbnail.rb +35 -0
- data/lib/iiif_s3/utilities.rb +12 -0
- data/lib/iiif_s3/utilities/helpers.rb +96 -0
- data/lib/iiif_s3/utilities/pdf_splitter.rb +50 -0
- data/lib/iiif_s3/version.rb +5 -0
- data/lib/wax_iiif.rb +83 -0
- data/spec/base_properties_spec.rb +22 -0
- data/spec/data/blank.csv +0 -0
- data/spec/data/invalid.csv +1 -0
- data/spec/data/no_header.csv +1 -0
- data/spec/data/test.csv +2 -0
- data/spec/data/test.jpg +0 -0
- data/spec/data/test.pdf +0 -0
- data/spec/iiif_s3/builder_spec.rb +152 -0
- data/spec/iiif_s3/collection_spec.rb +68 -0
- data/spec/iiif_s3/config_spec.rb +15 -0
- data/spec/iiif_s3/image_info_spec.rb +57 -0
- data/spec/iiif_s3/image_record_spec.rb +96 -0
- data/spec/iiif_s3/image_variant_spec.rb +71 -0
- data/spec/iiif_s3/manifest_spec.rb +97 -0
- data/spec/iiif_s3/utilities/pdf_splitter_spec.rb +17 -0
- data/spec/shared_contexts.rb +115 -0
- data/spec/spec_helper.rb +12 -0
- data/test.rb +77 -0
- data/wax_iiif.gemspec +29 -0
- metadata +218 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
|
2
|
+
require "mini_magick"
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module IiifS3
|
6
|
+
|
7
|
+
#
|
8
|
+
# Class ImageTile is a specific ImageVariant used when generating a
|
9
|
+
# stack of tiles suitable for Mirador-style zooming interfaces. Each
|
10
|
+
# instance of ImageTile represents a single tile.
|
11
|
+
#
|
12
|
+
# @author David Newbury <david.newbury@gmail.com>
|
13
|
+
#
|
14
|
+
class ImageTile < ImageVariant
|
15
|
+
|
16
|
+
#
|
17
|
+
# Initializing this
|
18
|
+
#
|
19
|
+
# @param [Hash] data A Image Data object.
|
20
|
+
# @param [IiifS3::Config] config The configuration object
|
21
|
+
# @param [Hash<width: Number, height: Number, x Number, y: Number, xSize: Number, ySize: Number>] tile
|
22
|
+
# A hash of parameters that defines this tile.
|
23
|
+
def initialize(data, config, tile)
|
24
|
+
@tile = tile
|
25
|
+
super(data, config)
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def resize(width=nil,height=nil)
|
31
|
+
@image.combine_options do |img|
|
32
|
+
img.crop "#{@tile[:width]}x#{@tile[:height]}+#{@tile[:x]}+#{@tile[:y]}"
|
33
|
+
img.resize "#{@tile[:xSize]}x#{@tile[:ySize]}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def region
|
38
|
+
"#{@tile[:x]},#{@tile[:y]},#{@tile[:width]},#{@tile[:height]}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def filestring
|
42
|
+
"/#{region}/#{@tile[:xSize]},/0"
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
|
2
|
+
require "mini_magick"
|
3
|
+
require 'fileutils'
|
4
|
+
require_relative "utilities"
|
5
|
+
|
6
|
+
module IiifS3
|
7
|
+
|
8
|
+
FakeImageVariant = Struct.new(:id, :width, :height, :uri, :mime_type)
|
9
|
+
|
10
|
+
#
|
11
|
+
# Class ImageVariant represents a single image file within a manifest.
|
12
|
+
#
|
13
|
+
#
|
14
|
+
# @author David Newbury <david.newbury@gmail.com>
|
15
|
+
#
|
16
|
+
class ImageVariant
|
17
|
+
include Utilities::Helpers
|
18
|
+
include MiniMagick
|
19
|
+
|
20
|
+
#
|
21
|
+
# Initializing an ImageVariant will create the actual image file
|
22
|
+
# on the file system.
|
23
|
+
#
|
24
|
+
# To initialize an image, you will need the
|
25
|
+
# data hash to have an "id", a "image_path", and a "page_number".
|
26
|
+
#
|
27
|
+
# @param [Hash] data A Image Data object.
|
28
|
+
# @param [IiifS3::Config] config The configuration object
|
29
|
+
# @param [Number] width the desired width of this object in pixels
|
30
|
+
# @param [Number] height the desired height of this object in pixels
|
31
|
+
# @raise IiifS3::Error::InvalidImageData
|
32
|
+
#
|
33
|
+
def initialize(data, config, width = nil, height = nil)
|
34
|
+
|
35
|
+
@config = config
|
36
|
+
# Validate input data
|
37
|
+
if data.id.nil? || data.id.to_s.empty?
|
38
|
+
raise IiifS3::Error::InvalidImageData, "Each image needs an ID"
|
39
|
+
elsif data.image_path.nil? || data.image_path.to_s.empty?
|
40
|
+
raise IiifS3::Error::InvalidImageData, "Each image needs an path."
|
41
|
+
end
|
42
|
+
|
43
|
+
# open image
|
44
|
+
begin
|
45
|
+
@image = Image.open(data.image_path)
|
46
|
+
rescue MiniMagick::Invalid => e
|
47
|
+
raise IiifS3::Error::InvalidImageData, "Cannot read this image file: #{data.image_path}. #{e}"
|
48
|
+
end
|
49
|
+
|
50
|
+
resize(width, height)
|
51
|
+
@image.format "jpg"
|
52
|
+
|
53
|
+
@id = generate_image_id(data.id,data.page_number)
|
54
|
+
@uri = "#{id}#{filestring}/default.jpg"
|
55
|
+
|
56
|
+
# Create the on-disk version of the file
|
57
|
+
path = "#{generate_image_location(data.id,data.page_number)}#{filestring}"
|
58
|
+
FileUtils::mkdir_p path
|
59
|
+
filename = "#{path}/default.jpg"
|
60
|
+
@image.write filename unless File.exists? filename
|
61
|
+
add_file_to_s3(filename) if @config.upload_to_s3
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
# @!attribute [r] uri
|
66
|
+
# @return [String] The URI for the jpeg image
|
67
|
+
attr_reader :uri
|
68
|
+
|
69
|
+
#
|
70
|
+
# @!attribute [r] id
|
71
|
+
# @return [String] The URI for the variant.
|
72
|
+
attr_reader :id
|
73
|
+
|
74
|
+
|
75
|
+
# Get the image width
|
76
|
+
#
|
77
|
+
#
|
78
|
+
# @return [Number] The width of the image in pixels
|
79
|
+
def width
|
80
|
+
@image.width
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get the image height
|
84
|
+
#
|
85
|
+
#
|
86
|
+
# @return [Number] The height of the image in pixels
|
87
|
+
def height
|
88
|
+
@image.height
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# Get the MIME Content-Type of the image.
|
93
|
+
#
|
94
|
+
# @return [String] the MIME Content-Type (typically "image/jpeg")
|
95
|
+
#
|
96
|
+
def mime_type
|
97
|
+
@image.mime_type
|
98
|
+
end
|
99
|
+
|
100
|
+
# Generate a URI for an image
|
101
|
+
#
|
102
|
+
# @param [String] id The specific ID for the image
|
103
|
+
# @param [String, Number] page_number The page number for this particular image.
|
104
|
+
#
|
105
|
+
# @return [<type>] <description>
|
106
|
+
#
|
107
|
+
def generate_image_id(id, page_number)
|
108
|
+
"#{@config.base_url}#{@config.prefix}/#{@config.image_directory_name}/#{id}-#{page_number}"
|
109
|
+
end
|
110
|
+
|
111
|
+
protected
|
112
|
+
|
113
|
+
def region
|
114
|
+
"full"
|
115
|
+
end
|
116
|
+
|
117
|
+
def resize(width, height)
|
118
|
+
@image.resize "#{width}x#{height}"
|
119
|
+
end
|
120
|
+
|
121
|
+
def filestring
|
122
|
+
"/#{region}/#{width},/0"
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module IiifS3
|
2
|
+
|
3
|
+
FakeManifest = Struct.new(:id, :type, :label)
|
4
|
+
|
5
|
+
#
|
6
|
+
# Class Manifest is an abstraction over the IIIF Manifest, and by extension over the
|
7
|
+
# entire Presentation API. It takes the internal representation of data and converts
|
8
|
+
# it into a collection of JSON-LD documents. Optionally, it also provides the ability
|
9
|
+
# to save these files to disk and upload them to Amazon S3.
|
10
|
+
#
|
11
|
+
# @author David Newbury <david.newbury@gmail.com>
|
12
|
+
#
|
13
|
+
|
14
|
+
class Manifest
|
15
|
+
|
16
|
+
# @return [String] The IIIF default type for a manifest.
|
17
|
+
TYPE = "sc:Manifest"
|
18
|
+
|
19
|
+
include BaseProperties
|
20
|
+
|
21
|
+
#--------------------------------------------------------------------------
|
22
|
+
# CONSTRUCTOR
|
23
|
+
#--------------------------------------------------------------------------
|
24
|
+
|
25
|
+
# This will initialize a new manifest.
|
26
|
+
#
|
27
|
+
# @param [Array<ImageRecord>] image_records An array of ImageRecord types
|
28
|
+
# @param [<type>] config <description>
|
29
|
+
# @param [<type>] opts <description>
|
30
|
+
#
|
31
|
+
def initialize(image_records,config, opts = {})
|
32
|
+
@config = config
|
33
|
+
image_records.each do |record|
|
34
|
+
raise IiifS3::Error::InvalidImageData, "The data provided to the manifest were not ImageRecords" unless record.is_a? ImageRecord
|
35
|
+
end
|
36
|
+
|
37
|
+
@primary = image_records.find{|obj| obj.is_primary}
|
38
|
+
raise IiifS3::Error::InvalidImageData, "No 'is_primary' was found in the image data." unless @primary
|
39
|
+
raise IiifS3::Error::MultiplePrimaryImages, "Multiple primary images were found in the image data." unless image_records.count{|obj| obj.is_primary} == 1
|
40
|
+
|
41
|
+
self.id = "#{@primary.id}/manifest"
|
42
|
+
self.label = @primary.label || opts[:label] || ""
|
43
|
+
self.description = @primary.description || opts[:description]
|
44
|
+
self.attribution = @primary.attribution || opts.fetch(:attribution, nil)
|
45
|
+
self.logo = @primary.logo || opts.fetch(:logo, nil)
|
46
|
+
self.license = @primary.license || opts.fetch(:license, nil)
|
47
|
+
self.metadata = @primary.metadata || opts.fetch(:metadata, nil)
|
48
|
+
|
49
|
+
@sequences = build_sequence(image_records)
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# @return [String] the JSON-LD representation of the manifest as a string.
|
54
|
+
#
|
55
|
+
def to_json
|
56
|
+
obj = base_properties
|
57
|
+
|
58
|
+
obj["thumbnail"] = @primary.variants["thumbnail"].uri
|
59
|
+
obj["viewingDirection"] = @primary.viewing_direction
|
60
|
+
obj["viewingHint"] = @primary.is_document ? "paged" : "individuals"
|
61
|
+
obj["sequences"] = [@sequences]
|
62
|
+
|
63
|
+
return JSON.pretty_generate obj
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Save the manifest and all sub-resources to disk, using the
|
68
|
+
# paths contained withing the IiifS3::Config object passed at
|
69
|
+
# initialization.
|
70
|
+
#
|
71
|
+
# Will create the manifest, sequences, canvases, and annotation subobjects.
|
72
|
+
#
|
73
|
+
# @return [Void]
|
74
|
+
#
|
75
|
+
def save_all_files_to_disk
|
76
|
+
data = JSON.parse(self.to_json)
|
77
|
+
save_to_disk(data)
|
78
|
+
data["sequences"].each do |sequence|
|
79
|
+
save_to_disk(sequence)
|
80
|
+
sequence["canvases"].each do |canvas|
|
81
|
+
save_to_disk(canvas)
|
82
|
+
canvas["images"].each do |annotation|
|
83
|
+
save_to_disk(annotation)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
return nil
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
|
92
|
+
|
93
|
+
#--------------------------------------------------------------------------
|
94
|
+
def build_sequence(image_records,opts = {name: DEFAULT_SEQUENCE_NAME})
|
95
|
+
name = opts.delete(:name)
|
96
|
+
seq_id = generate_id "#{@primary.id}/sequence/#{name}"
|
97
|
+
|
98
|
+
opts.merge({
|
99
|
+
"@id" => seq_id,
|
100
|
+
"@type" => SEQUENCE_TYPE,
|
101
|
+
"canvases" => image_records.collect {|image_record| build_canvas(image_record)}
|
102
|
+
})
|
103
|
+
end
|
104
|
+
|
105
|
+
#--------------------------------------------------------------------------
|
106
|
+
def build_canvas(data)
|
107
|
+
|
108
|
+
canvas_id = generate_id "#{data.id}/canvas/#{data.section}"
|
109
|
+
|
110
|
+
obj = {
|
111
|
+
"@type" => CANVAS_TYPE,
|
112
|
+
"@id" => canvas_id,
|
113
|
+
"label" => data.section_label,
|
114
|
+
"width" => data.variants["full"].width.floor,
|
115
|
+
"height" => data.variants["full"].height.floor,
|
116
|
+
"thumbnail" => data.variants["thumbnail"].uri
|
117
|
+
}
|
118
|
+
obj["images"] = [build_image(data, obj)]
|
119
|
+
|
120
|
+
# handle objects that are less than 1200px on a side by doubling canvas size
|
121
|
+
if obj["width"] < MIN_CANVAS_SIZE || obj["height"] < MIN_CANVAS_SIZE
|
122
|
+
obj["width"] *= 2
|
123
|
+
obj["height"] *= 2
|
124
|
+
end
|
125
|
+
return obj
|
126
|
+
end
|
127
|
+
|
128
|
+
#--------------------------------------------------------------------------
|
129
|
+
def build_image(data, canvas)
|
130
|
+
annotation_id = generate_id "#{data.id}/annotation/#{data.section}"
|
131
|
+
{
|
132
|
+
"@type" => ANNOTATION_TYPE,
|
133
|
+
"@id" => annotation_id,
|
134
|
+
"motivation" => MOTIVATION,
|
135
|
+
"resource" => {
|
136
|
+
"@id" => data.variants["full"].uri,
|
137
|
+
"@type" => IMAGE_TYPE,
|
138
|
+
"format" => data.variants["full"].mime_type || "image/jpeg",
|
139
|
+
"service" => {
|
140
|
+
"@context" => IiifS3::IMAGE_CONTEXT,
|
141
|
+
"@id" => data.variants["full"].id,
|
142
|
+
"profile" => IiifS3::LEVEL_0,
|
143
|
+
},
|
144
|
+
"width" => data.variants["full"].width,
|
145
|
+
"height" => data.variants["full"].height,
|
146
|
+
},
|
147
|
+
"on" => canvas["@id"]
|
148
|
+
}
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "mini_magick"
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module IiifS3
|
5
|
+
|
6
|
+
#
|
7
|
+
# Class Thumbnail provides a specific variant of an image file used for the thumbnail links
|
8
|
+
# within the metadata. It will generate a consistent sized version based on a max width
|
9
|
+
# and height. By default, it generates images at 250px on the longest size.
|
10
|
+
#
|
11
|
+
# @author David Newbury <david.newbury@gmail.com>
|
12
|
+
#
|
13
|
+
class Thumbnail < ImageVariant
|
14
|
+
|
15
|
+
# Initialize a new thumbnail.
|
16
|
+
#
|
17
|
+
# @param [hash] data The image data object
|
18
|
+
# @param [Hash] config The configuration hash
|
19
|
+
# @param [Integer] max_width The maximum width of the thumbnail
|
20
|
+
# @param [Integer] max_height The maximum height of the thumbnail
|
21
|
+
#
|
22
|
+
def initialize(data, config, max_width=nil, max_height = nil)
|
23
|
+
@max_width = max_width || config.thumbnail_size
|
24
|
+
@max_height = max_height || config.thumbnail_size
|
25
|
+
super(data,config)
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def resize(width, height)
|
31
|
+
@image.resize "#{@max_width}x#{@max_height}"
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require_relative "utilities/pdf_splitter"
|
2
|
+
require_relative "utilities/helpers"
|
3
|
+
|
4
|
+
module IiifS3
|
5
|
+
# Module Utilities provides a set of basic utilities and helper functions for
|
6
|
+
# the IIIFS3 library.
|
7
|
+
#
|
8
|
+
# @author David Newbury <david.newbury@gmail.com>
|
9
|
+
#
|
10
|
+
module Utilities
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module IiifS3
|
2
|
+
module Utilities
|
3
|
+
|
4
|
+
# Module Helpers provides helper functions. Which seems logical.
|
5
|
+
#
|
6
|
+
# Note that these functions require an @config object to exist on the
|
7
|
+
# mixed-in class.
|
8
|
+
#
|
9
|
+
# @author David Newbury <david.newbury@gmail.com>
|
10
|
+
#
|
11
|
+
module Helpers
|
12
|
+
|
13
|
+
# def self.included(klass)
|
14
|
+
# unless respond_to? :config
|
15
|
+
# raise StandardError, "The helpers have been included in class #{klass}, but #{klass} does not have a @config object."
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
|
19
|
+
# This will generate a valid, escaped URI for an object.
|
20
|
+
#
|
21
|
+
# This will prepend the standard path and prefix, and will append .json
|
22
|
+
# if enabled.
|
23
|
+
#
|
24
|
+
# @param [String] path The desired ID string
|
25
|
+
# @return [String] The generated URI
|
26
|
+
def generate_id(path)
|
27
|
+
val = "#{@config.base_url}#{@config.prefix}/#{path}"
|
28
|
+
val += ".json" if @config.use_extensions
|
29
|
+
URI.escape(val)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Given an id, generate a path on disk for that id, based on the config file
|
33
|
+
#
|
34
|
+
# @param [String] id the path to the unique key for the object
|
35
|
+
# @return [String] a path within the output dir, with the prefix included
|
36
|
+
def generate_build_location(id)
|
37
|
+
"#{@config.output_dir}#{@config.prefix}/#{id}"
|
38
|
+
end
|
39
|
+
|
40
|
+
# Given an id and a page number, generate a path on disk for an image
|
41
|
+
# The path will be based on the config file.
|
42
|
+
#
|
43
|
+
# @param [String] id the unique key for the object
|
44
|
+
# @param [String] page_number the page for this image.
|
45
|
+
# @return [String] a path for the image
|
46
|
+
def generate_image_location(id, page_number)
|
47
|
+
generate_build_location "#{@config.image_directory_name}/#{id}-#{page_number}"
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
def get_data_path(data)
|
52
|
+
data['@id'].gsub(@config.base_url,@config.output_dir)
|
53
|
+
end
|
54
|
+
|
55
|
+
def save_to_disk(data)
|
56
|
+
path = get_data_path(data)
|
57
|
+
data["@context"] ||= IiifS3::PRESENTATION_CONTEXT
|
58
|
+
puts "writing #{path}" if @config.verbose?
|
59
|
+
FileUtils::mkdir_p File.dirname(path)
|
60
|
+
File.open(path, "w") do |file|
|
61
|
+
file.puts JSON.pretty_generate(data)
|
62
|
+
end
|
63
|
+
add_file_to_s3(path) if @config.upload_to_s3
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_s3_key(filename)
|
67
|
+
key = filename.gsub(@config.output_dir,"")
|
68
|
+
key = key[1..-1] if key[0] == "/"
|
69
|
+
end
|
70
|
+
|
71
|
+
def add_file_to_s3(filename)
|
72
|
+
key = get_s3_key(filename)
|
73
|
+
if File.extname(filename) == ".json" || File.extname(filename) == ""
|
74
|
+
@config.s3.add_json(key,filename)
|
75
|
+
elsif File.extname(filename) == ".jpg"
|
76
|
+
@config.s3.add_image(key,filename)
|
77
|
+
else
|
78
|
+
raise "Cannot identify file type!"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def add_default_redirect(filename)
|
83
|
+
key = filename.gsub(@config.output_dir,"")
|
84
|
+
key = key[1..-1] if key[0] == "/"
|
85
|
+
|
86
|
+
name_key = key.split(".")[0..-2].join(".")
|
87
|
+
|
88
|
+
unless key == name_key
|
89
|
+
key = "#{@config.base_url}/#{key}"
|
90
|
+
puts "adding redirect from #{name_key} to #{key}" if @config.verbose?
|
91
|
+
@config.s3.add_redirect(name_key, key)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|