aperitiiif 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 02f6e4b4232068a021694eadacda55cde5757f54811d297bf0e73982bb50d0c1
4
+ data.tar.gz: ec553c98daa97c74fd8a3a315a8ec68e316ed3d5450b6a83d00c8bd5d38c97c5
5
+ SHA512:
6
+ metadata.gz: 5628ed423c165523a95ea40ca4a6a43b67ce791142f45c927b6858b1ae0bf7cb64ef33906589a07cae08d6f1e512cde751f940ec05db2316a2df2ad232634aad
7
+ data.tar.gz: 24b053c916baa29fde6dda1ca17b2d5a49356157217de7fe60271aad477b10792b65e12519a44ea5efb62ec84d8e9c9663bc8204d706e68d36db232c252ebc2a
data/.reek.yml ADDED
@@ -0,0 +1,10 @@
1
+ ---
2
+ detectors:
3
+ TooManyStatements:
4
+ max_statements: 6
5
+ DuplicateMethodCall:
6
+ allow_calls:
7
+ - options
8
+ - match
9
+ UtilityFunction:
10
+ enabled: false
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --format documentation
3
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,28 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.1
3
+ SuggestExtensions: false
4
+ NewCops: disable
5
+
6
+ Style/StringLiterals:
7
+ Enabled: true
8
+ EnforcedStyle: single_quotes
9
+
10
+ Style/IfUnlessModifier:
11
+ Enabled: false
12
+
13
+ Style/StringLiteralsInInterpolation:
14
+ Enabled: true
15
+ EnforcedStyle: single_quotes
16
+
17
+ Layout/LineLength:
18
+ Max: 120
19
+ AllowedPatterns:
20
+ - 'raise'
21
+
22
+ Naming/RescuedExceptionsVariableName:
23
+ Enabled: false # conflicts with reek expectations
24
+
25
+ Metrics/BlockLength:
26
+ Exclude:
27
+ - '*.gemspec'
28
+ - 'spec/**/*'
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.1.2
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rake', '~> 13.0'
8
+
9
+ group :test do
10
+ gem 'reek'
11
+ gem 'rspec'
12
+ gem 'rubocop', '~> 1.21'
13
+ gem 'simplecov', require: false
14
+ end
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # aperitiiif-cli
2
+
3
+ gem-packaged commands for processing aperitiiif batches 🥂
4
+
5
+ [![rspec](https://github.com/nyu-dss/aperitiiif-cli/actions/workflows/rspec.yml/badge.svg)](https://github.com/nyu-dss/aperitiiif/actions/workflows/rspec.yml) [![reek](https://github.com/nyu-dss/aperitiiif-cli/actions/workflows/reek.yml/badge.svg)](https://github.com/nyu-dss/aperitiiif/actions/workflows/reek.yml) [![rubocop](https://github.com/nyu-dss/aperitiiif-cli/actions/workflows/rubocop.yml/badge.svg)](https://github.com/nyu-dss/aperitiiif/actions/workflows/rubocop.yml)
6
+
7
+ [![Maintainability](https://api.codeclimate.com/v1/badges/c25005f1fd12e7a86122/maintainability)](https://codeclimate.com/github/nyu-dss/aperitiiif/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/c25005f1fd12e7a86122/test_coverage)](https://codeclimate.com/github/nyu-dss/aperitiiif/test_coverage)
8
+
9
+ ## Overview
10
+
11
+ <img alt="aperitiiif service diagram" src="https://nyu-dss.github.io/aperitiiif/media/aperitiiif.jpg" style="max-height:300px;width:auto" />
12
+
13
+ > View as [PDF](https://nyu-dss.github.io/aperitiiif/media/aperitiiif.pdf).
14
+
15
+
16
+ ## Prerequisites
17
+ - [Ruby Version Manager](https://rvm.io/rvm/install)
18
+ - [Git](https://git-scm.com/downloads)
19
+ - [Vips](https://www.libvips.org/install.html)
20
+
21
+ ## Installation
22
+
23
+ ### Recommended
24
+
25
+ It is highly recommended that you use the [aperitiiif-batch-template](https://github.com/nyu-dss/aperitiiif-batch-template) repo to create your new batch project. This method will include all the necessary Ruby dependencies and project structure.
26
+
27
+ ### Manual
28
+
29
+ Alternatively, you can add the gem to your project's Gemfile:
30
+
31
+ ``` ruby
32
+ gem 'aperitiiif', github: 'nyu-dss/aperitiiif-cli'
33
+ ```
34
+
35
+ Then install by running the command:
36
+
37
+ ``` sh
38
+ bundle install
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ After your batch project is set up and you have installed the dependencies using Bundler, you will have access to the `aperitiiif` commands.
44
+
45
+ 1. Check available commands
46
+ ```sh
47
+ bundle exec aperitiiif --help
48
+ ```
49
+ 2. Check available batch commands
50
+ ```sh
51
+ bundle exec aperitiiif batch --help
52
+ ```
53
+ You will see something like:
54
+ ```sh
55
+ ➜ bundle exec aperitiiif batch --help
56
+ Commands:
57
+ aperitiiif batch build # build batch resources
58
+ aperitiiif batch help [COMMAND] # Describe subcommands or one specific subc...
59
+ aperitiiif batch lint # lint the batch
60
+ aperitiiif batch reset # reset the batch
61
+ ```
62
+
63
+ ## Development
64
+
65
+ Contributions should:
66
+ - Avoid code smells
67
+ - Follow style guide
68
+ - Update tests as needed
69
+ - Update documentation as needed
70
+
71
+ To ensure the above, this repo includes configuration for [reek](https://github.com/troessner/reek) and [rubocop](https://github.com/rubocop/rubocop) as well as [rspec](https://rspec.info/) tests. You can run them respectively with the following commands:
72
+
73
+ ```sh
74
+ bundle exec reek
75
+ ```
76
+ ```sh
77
+ bundle exec rubocop -A
78
+ ```
79
+ ```sh
80
+ bundle exec rspec
81
+ ```
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'reek/rake/task'
5
+ require 'rspec/core/rake_task'
6
+
7
+ begin
8
+ RSpec::Core::RakeTask.new :spec do |t|
9
+ t.fail_on_error = false
10
+ end
11
+
12
+ Reek::Rake::Task.new do |t|
13
+ t.fail_on_error = false
14
+ end
15
+
16
+ task default: :spec
17
+ end
data/bin/aperitiiif ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ ENV['VIPS_WARNING'] = '1'
5
+
6
+ require 'aperitiiif'
7
+
8
+ Aperitiiif::CLI::Main.start
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'iiif/presentation'
4
+ require 'mimemagic'
5
+ require 'vips'
6
+
7
+ # to do
8
+ module Aperitiiif
9
+ # to do
10
+ # has smell :reek:TooManyMethods
11
+ class Asset
12
+ attr_reader :parent_id
13
+
14
+ TARGET_EXT = '.jpg'
15
+
16
+ def initialize(parent_id, source, config)
17
+ @parent_id = parent_id
18
+ @source = source
19
+ @config = config
20
+
21
+ validate_source
22
+ validate_config
23
+ end
24
+
25
+ def validate_source
26
+ raise Aperitiiif::Error, "Asset source #{@source} does not exist" unless File.file? @source
27
+ raise Aperitiiif::Error, "Asset source #{@source} is not a valid format" unless Utils.valid_source? @source
28
+ end
29
+
30
+ def validate_config
31
+ raise Aperitiiif::Error, "No value found for 'namespace'. Check your config?" if @config.namespace.empty?
32
+ raise Aperitiiif::Error, "No value found for 'image_build_dir'. Check your config?" if @config.image_build_dir.empty?
33
+ raise Aperitiiif::Error, "No value found for 'presentation_api_url'. Check your config?" if @config.presentation_api_url.empty?
34
+ raise Aperitiiif::Error, "No value found for 'image_api_url'. Check your config?" if @config.image_api_url.empty?
35
+ end
36
+
37
+ def id
38
+ @id ||= build_id
39
+ end
40
+
41
+ def target
42
+ @target ||= build_target
43
+ end
44
+
45
+ def width
46
+ @width ||= Vips::Image.new_from_file(@source).width
47
+ end
48
+
49
+ def height
50
+ @height ||= Vips::Image.new_from_file(@source).height
51
+ end
52
+
53
+ def target_mime
54
+ write_to_target
55
+ @target_mime ||= Utils.mime target
56
+ end
57
+
58
+ def source_mime
59
+ @source_mime ||= Utils.mime @source
60
+ end
61
+
62
+ def canvas_url
63
+ @canvas_url ||= "#{@config.presentation_api_url}/canvas/#{id}.json"
64
+ end
65
+
66
+ def thumbnail_url
67
+ @thumbnail_url ||= "#{@config.image_api_url}/#{id}/full/250,/0/default.jpg"
68
+ end
69
+
70
+ def annotation_url
71
+ @annotation_url ||= "#{@config.presentation_api_url}/annotation/#{id}.json"
72
+ end
73
+
74
+ def full_resource_url
75
+ @full_resource_url ||= "#{@config.image_api_url}/#{id}/full/full/0/default.jpg"
76
+ end
77
+
78
+ def service_url
79
+ @service_url ||= "#{@config.image_api_url}/#{id}"
80
+ end
81
+
82
+ def build_id
83
+ id = Utils.basename_no_ext @source
84
+ id.prepend "#{@parent_id}_" unless id == @parent_id
85
+ id.prepend "#{@config.namespace}_"
86
+ end
87
+
88
+ def target_written? = File.file? target
89
+
90
+ def build_target = "#{@config.image_build_dir}/#{id}#{TARGET_EXT}"
91
+
92
+ def write_to_target
93
+ return false if target_written?
94
+
95
+ FileUtils.mkdir_p @config.image_build_dir
96
+ Vips::Image.new_from_file(@source).write_to_file target
97
+ end
98
+
99
+ def service
100
+ {
101
+ '@context' => 'http://iiif.io/api/image/2/context.json',
102
+ '@id' => service_url
103
+ }
104
+ end
105
+
106
+ def annotation
107
+ opts = {
108
+ '@id' => annotation_url,
109
+ 'on' => canvas_url,
110
+ 'resource' => resource
111
+ }
112
+ IIIF::Presentation::Annotation.new opts
113
+ end
114
+
115
+ def resource
116
+ opts = {
117
+ '@id' => full_resource_url,
118
+ '@type' => 'dcterms:Image',
119
+ 'service' => service,
120
+ 'format' => target_mime
121
+ }
122
+ IIIF::Presentation::Resource.new opts
123
+ end
124
+
125
+ def canvas
126
+ opts = {
127
+ '@id' => canvas_url,
128
+ 'label' => id,
129
+ 'width' => width,
130
+ 'height' => height,
131
+ 'thumbnail' => thumbnail_url,
132
+ 'images' => [annotation]
133
+ }
134
+ IIIF::Presentation::Canvas.new opts
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TO DO COMMENT
4
+ module Aperitiiif
5
+ # TO DO COMMENT
6
+ module Assets
7
+ def assets
8
+ @assets ||= map_to_assets
9
+ end
10
+
11
+ def child_assets(path)
12
+ return [path] if Utils.valid_source?(path)
13
+ return [] unless File.directory? path
14
+
15
+ Dir.glob("#{path}/*").select { |sub| Utils.valid_source? sub }
16
+ end
17
+
18
+ # has smell :reek:NestedIterators
19
+ def map_to_assets(mymap = asset_map)
20
+ mymap.flat_map { |id, vals| vals.map { |val| Asset.new id, val, config } }
21
+ end
22
+
23
+ def asset_map(dir = config.source_dir)
24
+ map = {}
25
+ Dir["#{dir}/*"].each do |path|
26
+ parent_id = Utils.parent_id(path, dir)
27
+ children = child_assets path
28
+ map[parent_id] = children unless children.empty?
29
+ end
30
+ map
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aperitiiif/item'
4
+
5
+ # TO DO COMMENT
6
+ module Aperitiiif
7
+ # TO DO COMMENT
8
+ module Items
9
+ def items
10
+ @items ||= items_from_assets
11
+ end
12
+
13
+ def items=(items)
14
+ @items = items
15
+ end
16
+
17
+ def items_from_assets(assets = self.assets)
18
+ grouped = assets.group_by(&:parent_id)
19
+ grouped.flat_map do |id, grouped_assets|
20
+ Item.new id, grouped_assets, config
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TO DO COMMENT
4
+ module Aperitiiif
5
+ # TO DO COMMENT
6
+ module Linters
7
+ def linters
8
+ @linters ||= [
9
+ method(:warn_nil_record_items),
10
+ method(:warn_duplicate_record_ids),
11
+ method(:warn_duplicate_image_names),
12
+ method(:warn_missing_labels),
13
+ method(:warn_stray_files)
14
+ ]
15
+ end
16
+
17
+ def lint
18
+ puts 'Linting...'.colorize(:cyan)
19
+ puts 'Passed ✓'.colorize(:green) unless report_failures.any?
20
+ end
21
+
22
+ def report_failures
23
+ linters.map(&:call)
24
+ end
25
+
26
+ # has smell :reek:TooManyStatements
27
+ def warn_nil_record_items
28
+ nil_record_items = items.select { |item| item.record.blank? }
29
+ return false if nil_record_items.empty?
30
+
31
+ warn "Could not find record(s) for #{nil_record_items.length} items:".colorze(:orange)
32
+ nil_record_items.each { |item| puts Utils.rm_batch_namespace(item.id).colorize(:yellow) }
33
+ true
34
+ end
35
+
36
+ # has smell :reek:TooManyStatements
37
+ def warn_duplicate_record_ids
38
+ ids = records.map(&:id)
39
+ dup_ids = ids.select { |id| ids.count(id) > 1 }.uniq
40
+ return false if dup_ids.empty?
41
+
42
+ warn "Found #{dup_ids.length} non-unique record id value(s):".colorze(:orange)
43
+ dup_ids.each { |id| puts id.colorize(:yellow) }
44
+ true
45
+ end
46
+
47
+ # has smell :reek:DuplicateMethodCall
48
+ # has smell :reek:TooManyStatements
49
+ # rubocop: disable Metrics/AbcSize
50
+ def warn_duplicate_image_names
51
+ files = Dir.glob("#{config.source_dir}/**/*").select { |file| File.file? file }
52
+ files.map! { |file| Utils.rm_ext file.sub(config.source_dir, '') }
53
+ duplicate_names = files.select { |file| files.count(file) > 1 }.uniq
54
+ return false if duplicate_names.empty?
55
+
56
+ warn "Found #{duplicate_names.length} duplicate image name(s):".colorze(:orange)
57
+ duplicate_names.each { |name| puts Utils.prune_prefix_junk(name).colorize(:yellow) }
58
+ true
59
+ end
60
+ # rubocop: enable Metrics/AbcSize
61
+
62
+ # has smell :reek:TooManyStatements
63
+ def warn_missing_labels
64
+ no_label_records = records.select { |record| record.label.to_s.empty? }
65
+ return false if no_label_records.empty?
66
+
67
+ warn "Found #{no_label_records.length} record(s) missing labels (strongly encouraged):".colorze(:orange)
68
+ no_label_records.each { |record| puts record.id.colorize(:yellow) }
69
+ true
70
+ end
71
+
72
+ # has smell :reek:TooManyStatements
73
+ # rubocop:disable Metrics/AbcSize
74
+ def warn_stray_files
75
+ stray_files = Dir.glob("#{config.source_dir}/**/*")
76
+ stray_files.select! { |file| File.file?(file) && !file.end_with?(*ALLOWED_SRC_FORMATS) }
77
+ return false if stray_files.empty?
78
+
79
+ warn "Found #{stray_files.length} stray file(s) with unaccepted file types:".colorze(:orange)
80
+ stray_files.each { |file| puts file.colorize(:yellow) }
81
+ puts "(Accepted file extensions: #{ALLOWED_SRC_FORMATS.join(', ')})".colorize(:pink)
82
+ true
83
+ end
84
+ # rubocop:enable Metrics/AbcSize
85
+ end
86
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TO DO COMMENT
4
+ module Aperitiiif
5
+ # TO DO COMMENT
6
+ module Records
7
+ def records
8
+ @records ||= records_from_file
9
+ end
10
+
11
+ def find_record(item_id, records = self.records)
12
+ records.find { |record| record.id == item_id }
13
+ end
14
+
15
+ # has smell :reek:DuplicateMethodCall
16
+ def records_from_file(file = config.records_file, defaults = config.records_defaults)
17
+ return [] unless records_file_configured? file
18
+ return [] unless records_file_exists? file
19
+
20
+ Utils.csv_records file, defaults
21
+ end
22
+
23
+ def records_file_configured?(file = config.records_file)
24
+ return true if file.present?
25
+
26
+ warn 'WARNING:: No records file configured'.colorize(:yellow)
27
+ false
28
+ end
29
+
30
+ def records_file_exists?(file = config.records_file)
31
+ return true if File.file?(file)
32
+
33
+ warn "WARNING:: Couldn't find records file #{file}".colorize(:yellow)
34
+ false
35
+ end
36
+
37
+ def load_records!
38
+ self.items = items.map do |item|
39
+ record = find_record(item.id, records)
40
+ item.record = record if record.present?
41
+ item
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'parallel'
5
+ require 'ruby-progressbar'
6
+ require 'safe_yaml'
7
+
8
+ require 'aperitiiif/batch/assets'
9
+ require 'aperitiiif/batch/items'
10
+ require 'aperitiiif/batch/records'
11
+ require 'aperitiiif/batch/linters'
12
+
13
+ # TO DO COMMENT
14
+ module Aperitiiif
15
+ # TO DO COMMENT
16
+ class Batch
17
+ include Assets
18
+ include Items
19
+ include Linters
20
+ include Records
21
+
22
+ def initialize; end
23
+
24
+ DEFAULT_CONFIG_FILE = './config.yml'
25
+
26
+ def config
27
+ @config ||= load_config_file
28
+ end
29
+
30
+ def load_config_file(file = DEFAULT_CONFIG_FILE)
31
+ @config = Config.new SafeYAML.load_file file
32
+ rescue StandardError
33
+ raise Aperitiiif::Error, "Cannot find file #{file}" unless File.file?(file)
34
+
35
+ raise Aperitiiif::Error, "Cannot read file #{file}. Is it valid yaml?"
36
+ end
37
+
38
+ def load_config_hash(hash)
39
+ @config = Config.new hash
40
+ end
41
+
42
+ def reset(dir = config.build_dir)
43
+ puts 'Resetting build...'.colorize(:cyan)
44
+ FileUtils.rm_rf dir
45
+ puts 'Done ✓'.colorize(:green)
46
+ end
47
+
48
+ def write_target_assets(assets = self.assets)
49
+ msg = 'Writing target image TIFs'.colorize(:cyan)
50
+ Parallel.map(assets, in_threads: 4, progress: { format: "#{msg}: %c/%u | %P%" }, &:write_to_target)
51
+ end
52
+
53
+ # has smell :reek:TooManyStatements
54
+ def write_presentation_json(items = self.items)
55
+ msg = 'Writing IIIF Presentation JSON'.colorize(:cyan)
56
+ load_records!
57
+ Parallel.map(items, in_threads: 4, progress: { format: "#{msg}: %c/%u | %P%" }, &:write_presentation_json)
58
+ write_iiif_collection_json
59
+ end
60
+
61
+ def seed
62
+ {
63
+ '@id' => iiif_collection_url,
64
+ 'label' => config.label,
65
+ 'description' => config.batch_description,
66
+ 'attribution' => config.batch_attribution
67
+ }.delete_if { |_key, val| val.blank? }
68
+ end
69
+
70
+ def iiif_collection
71
+ collection = IIIF::Presentation::Collection.new seed
72
+ collection.manifests = items.map(&:manifest)
73
+ collection
74
+ end
75
+
76
+ def iiif_collection_file = "#{config.presentation_build_dir}/#{config.namespace}/collection.json"
77
+ def iiif_collection_url = "#{config.presentation_api_url}/#{config.namespace}/collection.json"
78
+ def iiif_collection_written? = File.file? iiif_collection_file
79
+
80
+ def write_iiif_collection_json
81
+ Aperitiiif::Utils.mkfile_p iiif_collection_file, iiif_collection.to_json(pretty: true)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TO DO COMMENT
4
+ module Aperitiiif
5
+ # TO DO COMMENT
6
+ module CLI
7
+ # TO DO COMMENT
8
+ class SubCommandBase < Thor
9
+ def self.banner(command, _namespace, _subcommand)
10
+ "#{basename} #{subcommand_prefix} #{command.usage}"
11
+ end
12
+
13
+ def self.subcommand_prefix
14
+ str = name.gsub(/.*::/, '')
15
+ str.gsub!(/^[A-Z]/) { |match| match[0].downcase }
16
+ str.gsub!(/[A-Z]/) { |match| "-#{match[0].downcase}" }
17
+ str
18
+ end
19
+ end
20
+
21
+ # TO DO COMMENT
22
+ class Batch < SubCommandBase
23
+ desc 'build', 'build batch resources'
24
+
25
+ option :config, type: :string, default: ''
26
+ option :lint, type: :boolean
27
+ option :reset, type: :boolean
28
+
29
+ # has smells :reek:FeatureEnvy, :reek:TooManyStatements
30
+ # rubocop:disable Metrics/AbcSize
31
+ def build
32
+ batch = Aperitiiif::Batch.new
33
+ batch.load_config_file(options[:config]) if options[:config].present?
34
+ batch.reset if options[:reset]
35
+ batch.lint if options[:lint]
36
+
37
+ batch.write_target_assets
38
+ batch.write_presentation_json
39
+ index = Aperitiiif::Index.new batch
40
+ index.write type: :html
41
+ index.write type: :json
42
+ end
43
+ # rubocop:enable Metrics/AbcSize
44
+
45
+ desc 'reset', 'reset the batch'
46
+ def reset
47
+ batch = Aperitiiif::Batch.new
48
+ batch.reset
49
+ end
50
+
51
+ desc 'lint', 'lint the batch'
52
+ def lint
53
+ batch = Aperitiiif::Batch.new
54
+ batch.lint
55
+ end
56
+ end
57
+
58
+ # TO DO COMMENT
59
+ class Main < Thor
60
+ map %w[--version -v] => :__print_version
61
+ desc '--version, -v', 'print the version'
62
+ def __print_version
63
+ puts Aperitiiif::VERSION
64
+ end
65
+
66
+ desc 'batch SUBCOMMANDS', 'batch subcommands'
67
+ subcommand 'batch', Batch
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TO DO COMMENT
4
+ module Aperitiiif
5
+ # has smell :reek:Attribute
6
+ # TO DO COMMENT
7
+ class Config
8
+ attr_reader :namespace, :hash
9
+
10
+ DELEGATE = %i[puts p].freeze
11
+ DEFAULT_VALUES = {
12
+ 'build_dir' => './build',
13
+ 'source_dir' => './src/data',
14
+ 'service_namespace' => 'aperitiiif-batch'
15
+ }.freeze
16
+
17
+ def initialize(seed = {})
18
+ @hash = DEFAULT_VALUES.merge(seed)
19
+ @namespace = batch_namespace
20
+
21
+ validate
22
+ end
23
+
24
+ def method_missing(method, *args, &block)
25
+ return super if DELEGATE.include? method
26
+
27
+ @hash.fetch method.to_s, ''
28
+ end
29
+
30
+ def respond_to_missing?(method, _args)
31
+ DELEGATE.include?(method) or super
32
+ end
33
+
34
+ # should check for more reqs
35
+ def validate
36
+ raise Aperitiiif::Error, "Config is missing a valid 'namespace' for the batch." if namespace.empty?
37
+ raise Aperitiiif::Error, "Config is missing 'label' for the batch." if label.empty?
38
+ end
39
+
40
+ def batch_namespace(dir = nil)
41
+ dir ||= FileUtils.pwd
42
+ name = dir.split(service_namespace).last
43
+ Utils.slugify name
44
+ end
45
+
46
+ def presentation_build_dir = "#{build_dir}/presentation"
47
+ def image_build_dir = "#{build_dir}/image"
48
+ def html_build_dir = "#{build_dir}/html"
49
+ def records_file = @hash.dig('records', 'file') || ''
50
+ def records_defaults = @hash.dig('records', 'defaults') || {}
51
+ end
52
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorize'
4
+
5
+ # TO DO COMMENT
6
+ module Aperitiiif
7
+ # TO DO COMMENT
8
+ class Error < RuntimeError
9
+ def initialize(msg = '')
10
+ puts msg.colorize(:magenta) and super('')
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'liquid'
5
+
6
+ # TO DO COMMENT
7
+ module Aperitiiif
8
+ # TO DO COMMENT
9
+ class Index
10
+ VALID_TYPES = %i[json html].freeze
11
+ TEMPLATE_FILE = "#{__dir__}/template/index.html".freeze
12
+
13
+ attr_reader :config, :items
14
+
15
+ def initialize(batch)
16
+ @batch = batch
17
+ @config = batch.config
18
+ @items = batch.items
19
+ end
20
+
21
+ def path(type)
22
+ "#{@config.html_build_dir}/index.#{type}"
23
+ end
24
+
25
+ # rubocop: disable Metrics/AbcSize
26
+ # has smell :reek:TooManyStatements
27
+ def write(**options)
28
+ raise Aperitiiif::Error, "Index#write is missing required 'type:' option" unless options.key? :type
29
+ raise Aperitiiif::Error, "Index#write 'type: #{options[:type]}' does not match available types #{VALID_TYPES}" unless VALID_TYPES.include? options[:type]
30
+
31
+ print "Writing #{options[:type]} index...".colorize(:cyan)
32
+
33
+ index = options[:type] == :html ? to_html : to_json
34
+ path = options&.[](:path) || path(options[:type])
35
+
36
+ Aperitiiif::Utils.mkfile_p path, index
37
+
38
+ puts "\r#{"Writing #{options[:type]} index:".colorize(:cyan)} #{'Done ✓'.colorize(:green)} "
39
+ end
40
+ # rubocop: enable Metrics/AbcSize
41
+
42
+ def to_json(items = self.items)
43
+ JSON.pretty_generate items.map(&:to_hash)
44
+ end
45
+
46
+ # has smell :reek:DuplicateMethodCall
47
+ def to_html(items = self.items, config = self.config)
48
+ vars = {
49
+ 'items' => items.map(&:to_hash),
50
+ 'config' => config.hash,
51
+ 'iiif_collection_url' => @batch.iiif_collection_url,
52
+ 'formatted_date' => Aperitiiif::Utils.formatted_datetime
53
+ }
54
+ template = Liquid::Template.parse File.open(TEMPLATE_FILE).read
55
+ template.render vars
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'iiif/presentation'
4
+ require 'ostruct'
5
+
6
+ # to do
7
+ module Aperitiiif
8
+ # to do
9
+ # has smell :reek:InstanceVariableAssumption
10
+ # has smell :reek:TooManyInstanceVariables
11
+ # has smell :reek:TooManyMethods
12
+ class Item
13
+ # has smell :reek:Attribute
14
+ attr_writer :record
15
+ attr_reader :id, :assets
16
+
17
+ def initialize(id, assets, config)
18
+ @id = id
19
+ @config = config
20
+ @assets = assets
21
+
22
+ validate_config
23
+ end
24
+
25
+ def validate_config
26
+ raise Aperitiiif::Error, "No value found for 'namespace'. Check your config?" if @config.namespace.empty?
27
+ raise Aperitiiif::Error, "No value found for 'presentation_api_url'. Check your config?" if @config.presentation_api_url.empty?
28
+ raise Aperitiiif::Error, "No value found for 'presentation_build_dir'. Check your config?" if @config.presentation_build_dir.empty?
29
+ end
30
+
31
+ def record
32
+ @record || default_record
33
+ end
34
+
35
+ def default_record
36
+ Record.new({ 'id' => id }, @config.records_defaults)
37
+ end
38
+
39
+ def manifest
40
+ @manifest ||= build_manifest
41
+ end
42
+
43
+ def label
44
+ record.label
45
+ end
46
+
47
+ def manifest_url
48
+ "#{@config.presentation_api_url}/#{@config.namespace}/#{@id}/manifest.json"
49
+ end
50
+
51
+ def thumbnail_url
52
+ return '' if assets.empty?
53
+
54
+ assets.first.thumbnail_url
55
+ end
56
+
57
+ def viewpoint_url
58
+ "https://dss.hosting.nyu.edu/viewpoint/mirador/#manifests[]=#{CGI.escape manifest_url}&theme=dark"
59
+ end
60
+
61
+ # has smell :reek: TooManyStatements
62
+ def seed
63
+ {
64
+ '@id' => manifest_url,
65
+ 'label' => label,
66
+ 'logo' => record.logo,
67
+ 'description' => record.description,
68
+ 'source' => record.source,
69
+ 'metadata' => record.custom_metadata
70
+ }.delete_if { |_key, val| val.blank? }
71
+ end
72
+
73
+ def build_manifest
74
+ manifest = IIIF::Presentation::Manifest.new seed
75
+ sequence = IIIF::Presentation::Sequence.new
76
+ sequence.canvases = assets.map(&:canvas)
77
+ manifest.sequences << sequence
78
+ manifest
79
+ end
80
+
81
+ def manifest_file
82
+ "#{@config.presentation_build_dir}/#{@config.namespace}/#{id}/manifest.json"
83
+ end
84
+
85
+ def manifest_written?
86
+ File.file? manifest_file
87
+ end
88
+
89
+ def write_presentation_json
90
+ Aperitiiif::Utils.mkfile_p manifest_file, manifest.to_json(pretty: true)
91
+ end
92
+
93
+ def to_hash
94
+ {
95
+ 'id' => id,
96
+ 'label' => label,
97
+ 'manifest_url' => manifest_url,
98
+ 'thumbnail_url' => thumbnail_url,
99
+ 'viewpoint_url' => viewpoint_url
100
+ }
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ # to do
6
+ module Aperitiiif
7
+ # to do
8
+ class Record
9
+ DELEGATE = %i[puts p].freeze
10
+ CUSTOM_METADATA_PREFIX = 'meta.'
11
+
12
+ def initialize(hash, defaults = {})
13
+ @hash = defaults.merge(hash)
14
+ @id = id
15
+ end
16
+
17
+ def method_missing(method, *args, &block)
18
+ return super if DELEGATE.include? method
19
+
20
+ @hash.fetch method.to_s
21
+ end
22
+
23
+ def respond_to_missing?(method, _args)
24
+ DELEGATE.include?(method) or super
25
+ end
26
+
27
+ def id
28
+ @hash.fetch 'id'
29
+ rescue KeyError
30
+ raise Aperitiiif::Error, "Record has no 'id'\n#{@hash.inspect}"
31
+ end
32
+
33
+ def label = @hash.fetch 'label', id
34
+ def logo = @hash.fetch 'logo', ''
35
+ def description = @hash.fetch 'description', ''
36
+ def source = @hash.fetch 'source', ''
37
+
38
+ def custom_metadata_keys
39
+ @hash.to_h.keys.select { |key| key.to_s.start_with?(CUSTOM_METADATA_PREFIX) } || []
40
+ end
41
+
42
+ def custom_metadata
43
+ custom_metadata_keys.map do |key|
44
+ {
45
+ 'label' => key.to_s.delete_prefix(CUSTOM_METADATA_PREFIX),
46
+ 'value' => @hash[key]
47
+ }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html lang='en'>
3
+ <head>
4
+ <meta charset='UTF-8'>
5
+ <meta http-equiv='X-UA-Compatible' content='ie=edge'>
6
+ <meta name='viewport' content='width=device-width, initial-scale=1.0'>
7
+ <title>{{ config.label }} | aperitiiif batch listing</title>
8
+ <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css'>
9
+ <link rel='stylesheet' href='https://cdn.datatables.net/1.12.1/css/jquery.dataTables.min.css'>
10
+ </head>
11
+ <body>
12
+ <section class='hero is-info'>
13
+ <div class='hero-body'>
14
+ <div class='container'>
15
+ <h1 class='title'>{{ config.label }}</h1>
16
+ <p class='subtitle'>Aperitiiif Batch Index</p>
17
+ <p class='is-5 is-grey'>Last updated {{ formatted_date }}</p>
18
+ <p class='tags mt-5'>
19
+ <a target='_blank' class='tag is-danger is-light' href='{{ iiif_collection_url }}'>iiif collection</a>
20
+ <a target='_blank' class='tag is-link is-light' href='index.json'>index.json</a>
21
+ </p>
22
+ </div>
23
+ </div>
24
+ </section>
25
+ <section class='section'>
26
+ <div class='container'>
27
+ <h2 class='is-size-4 mb-3'>Items ({{ items.size }})</h2>
28
+ <div class='table-container'>
29
+ <table id='table' class='table display is-hoverable is-striped is-bordered'>
30
+ <thead>
31
+ <tr>
32
+ <td>item id</td>
33
+ <td>label</td>
34
+ <td>thumbnnail</td>
35
+ <td>iiif manifest</td>
36
+ <td>view in viewpoint</td>
37
+ </tr>
38
+ </thead>
39
+ <tbody>
40
+ {% for item in items %}
41
+ <tr>
42
+ <td><b>{{ item.id }}</b></td>
43
+ <td>{{ item.label }}</td>
44
+ <td><a target='_blank' href='{{ item.thumbnail_url }}'><img style="height:100px;width:auto" class='lazy' data-original="{{ item.thumbnail_url }}"/></a></td>
45
+ <td><a target='_blank' href='{{ item.manifest_url }}'><img alt='Thumbnail {{ item.label }}' src='https://upload.wikimedia.org/wikipedia/commons/e/e8/International_Image_Interoperability_Framework_logo.png' style="width:25px"/></a></td>
46
+ <td><a target='_blank' class='is-size-7' href='{{ item.viewpoint_url }}'>{{ item.viewpoint_url }}</a></td>
47
+ </tr>
48
+ {% endfor %}
49
+ </tbody>
50
+ </table>
51
+ </table>
52
+ </div>
53
+ </section>
54
+ <script src='https://code.jquery.com/jquery-3.5.1.js'></script>
55
+ <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery.lazyload/1.9.1/jquery.lazyload.min.js'></script>
56
+ <script src='https://cdn.datatables.net/1.12.1/js/jquery.dataTables.min.js'></script>
57
+ <script>
58
+ $(document).ready(function () {
59
+ $('#table').DataTable( {
60
+ drawCallback: function(){
61
+ $('img.lazy').lazyload();
62
+ }
63
+ });
64
+ });
65
+ </script>
66
+ </body>
67
+ </html>
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+
5
+ # TO DO COMMENT
6
+ module Aperitiiif
7
+ # TO DO COMMENT
8
+ module Utils
9
+ def self.basename_no_ext(str) = rm_ext File.basename(str)
10
+ def self.rm_ext(str) = str.sub File.extname(str), ''
11
+ def self.parent_id(str, dir) = prune_prefix_junk(rm_ext(str.sub(dir, '')))
12
+ def self.parent_dir(str) = File.dirname(str).split('/').last
13
+ def self.prune_prefix_junk(str) = str.sub(/^(_|\W+)/, '')
14
+ def self.mime(path) = MimeMagic.by_magic(File.open(path)).to_s
15
+ def self.valid_source?(path) = File.file?(path) && path.end_with?(*ALLOWED_SRC_FORMATS)
16
+
17
+ def self.slugify(str)
18
+ str.downcase!
19
+ str.strip!
20
+ str.gsub! ' ', '-'
21
+ str.gsub!(/[^\w-]/, '')
22
+ prune_prefix_junk str
23
+ end
24
+
25
+ def self.formatted_datetime(time = Time.now)
26
+ fmt_date = "#{time.month}/#{time.day}/#{time.year}"
27
+ fmt_time = "#{time.hour}:#{time.min.to_s.rjust(2, '0')}#{time.zone}"
28
+ "#{fmt_date} at #{fmt_time}"
29
+ end
30
+
31
+ def self.item_assets_from_path(path)
32
+ return [path] if Utils.valid_source?(path)
33
+ return [] unless File.directory? path
34
+
35
+ Dir.glob("#{path}/*").select { |sub| Utils.valid_source? sub }
36
+ end
37
+
38
+ def self.csv_records(file, defaults = {})
39
+ hashes = CSV.read(file, headers: true).map(&:to_hash)
40
+ hashes.map { |hash| Record.new hash.compact, defaults }
41
+ end
42
+
43
+ def self.mkfile_p(path, contents)
44
+ FileUtils.mkdir_p File.dirname(path)
45
+ File.open(path, 'w') { |file| file.write contents }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aperitiiif
4
+ VERSION = '0.1.0'
5
+ end
data/lib/aperitiiif.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorize'
4
+ require 'thor'
5
+
6
+ require 'aperitiiif/asset'
7
+ require 'aperitiiif/batch'
8
+ require 'aperitiiif/cli'
9
+ require 'aperitiiif/config'
10
+ require 'aperitiiif/error'
11
+ require 'aperitiiif/index'
12
+ require 'aperitiiif/item'
13
+ require 'aperitiiif/record'
14
+ require 'aperitiiif/version'
15
+ require 'aperitiiif/utils'
16
+
17
+ # TO DO COMMENT
18
+ module Aperitiiif
19
+ ALLOWED_SRC_FORMATS = %w[jpg jpeg png tif tiff].freeze
20
+ end
metadata ADDED
@@ -0,0 +1,195 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aperitiiif
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - mnyrop
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-09-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: colorize
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: iiif-presentation
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: liquid
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mimemagic
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: parallel
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: ruby-progressbar
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: ruby-vips
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: safe_yaml
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: thor
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description:
140
+ email:
141
+ - marii@nyu.edu
142
+ executables:
143
+ - aperitiiif
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - ".reek.yml"
148
+ - ".rspec"
149
+ - ".rubocop.yml"
150
+ - ".ruby-version"
151
+ - Gemfile
152
+ - README.md
153
+ - Rakefile
154
+ - bin/aperitiiif
155
+ - lib/aperitiiif.rb
156
+ - lib/aperitiiif/asset.rb
157
+ - lib/aperitiiif/batch.rb
158
+ - lib/aperitiiif/batch/assets.rb
159
+ - lib/aperitiiif/batch/items.rb
160
+ - lib/aperitiiif/batch/linters.rb
161
+ - lib/aperitiiif/batch/records.rb
162
+ - lib/aperitiiif/cli.rb
163
+ - lib/aperitiiif/config.rb
164
+ - lib/aperitiiif/error.rb
165
+ - lib/aperitiiif/index.rb
166
+ - lib/aperitiiif/item.rb
167
+ - lib/aperitiiif/record.rb
168
+ - lib/aperitiiif/template/index.html
169
+ - lib/aperitiiif/utils.rb
170
+ - lib/aperitiiif/version.rb
171
+ homepage: https://github.com/nyu-dss/aperitiiif-cli
172
+ licenses: []
173
+ metadata:
174
+ homepage_uri: https://github.com/nyu-dss/aperitiiif-cli
175
+ source_code_uri: https://github.com/nyu-dss/aperitiiif-cli
176
+ post_install_message:
177
+ rdoc_options: []
178
+ require_paths:
179
+ - lib
180
+ required_ruby_version: !ruby/object:Gem::Requirement
181
+ requirements:
182
+ - - ">="
183
+ - !ruby/object:Gem::Version
184
+ version: '3.1'
185
+ required_rubygems_version: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ requirements: []
191
+ rubygems_version: 3.3.7
192
+ signing_key:
193
+ specification_version: 4
194
+ summary: ''
195
+ test_files: []