wax_iiif 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|