aperitiiif 0.1.0

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