wax_iiif 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +29 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +11 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +10 -0
  7. data/LICENSE.md +30 -0
  8. data/README.md +8 -0
  9. data/Rakefile +24 -0
  10. data/lib/iiif_s3/base_properties.rb +95 -0
  11. data/lib/iiif_s3/builder.rb +254 -0
  12. data/lib/iiif_s3/collection.rb +61 -0
  13. data/lib/iiif_s3/config.rb +142 -0
  14. data/lib/iiif_s3/errors.rb +37 -0
  15. data/lib/iiif_s3/full_image.rb +20 -0
  16. data/lib/iiif_s3/image_info.rb +96 -0
  17. data/lib/iiif_s3/image_record.rb +141 -0
  18. data/lib/iiif_s3/image_tile.rb +46 -0
  19. data/lib/iiif_s3/image_variant.rb +126 -0
  20. data/lib/iiif_s3/manifest.rb +151 -0
  21. data/lib/iiif_s3/thumbnail.rb +35 -0
  22. data/lib/iiif_s3/utilities.rb +12 -0
  23. data/lib/iiif_s3/utilities/helpers.rb +96 -0
  24. data/lib/iiif_s3/utilities/pdf_splitter.rb +50 -0
  25. data/lib/iiif_s3/version.rb +5 -0
  26. data/lib/wax_iiif.rb +83 -0
  27. data/spec/base_properties_spec.rb +22 -0
  28. data/spec/data/blank.csv +0 -0
  29. data/spec/data/invalid.csv +1 -0
  30. data/spec/data/no_header.csv +1 -0
  31. data/spec/data/test.csv +2 -0
  32. data/spec/data/test.jpg +0 -0
  33. data/spec/data/test.pdf +0 -0
  34. data/spec/iiif_s3/builder_spec.rb +152 -0
  35. data/spec/iiif_s3/collection_spec.rb +68 -0
  36. data/spec/iiif_s3/config_spec.rb +15 -0
  37. data/spec/iiif_s3/image_info_spec.rb +57 -0
  38. data/spec/iiif_s3/image_record_spec.rb +96 -0
  39. data/spec/iiif_s3/image_variant_spec.rb +71 -0
  40. data/spec/iiif_s3/manifest_spec.rb +97 -0
  41. data/spec/iiif_s3/utilities/pdf_splitter_spec.rb +17 -0
  42. data/spec/shared_contexts.rb +115 -0
  43. data/spec/spec_helper.rb +12 -0
  44. data/test.rb +77 -0
  45. data/wax_iiif.gemspec +29 -0
  46. 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