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.
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