geoblacklight_admin 0.4.1 → 0.5.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 +4 -4
- data/README.md +10 -55
- data/Rakefile +3 -0
- data/app/assets/javascripts/geoblacklight_admin/chosen.js +7 -3
- data/app/assets/javascripts/geoblacklight_admin/datepicker.js +2 -2
- data/app/assets/javascripts/geoblacklight_admin/inputmask.js +2 -2
- data/app/assets/javascripts/geoblacklight_admin/truncate.js +2 -2
- data/app/assets/javascripts/geoblacklight_admin.js +1 -4
- data/app/assets/stylesheets/geoblacklight_admin/_core.scss +3 -1
- data/app/assets/stylesheets/geoblacklight_admin/modules/_images.scss +4 -0
- data/app/assets/stylesheets/geoblacklight_admin/modules/_nav.scss +4 -0
- data/app/assets/stylesheets/geoblacklight_admin/modules/_results.scss +5 -0
- data/app/controllers/admin/admin_controller.rb +1 -1
- data/app/controllers/admin/advanced_search_controller.rb +0 -1
- data/app/controllers/admin/assets_controller.rb +142 -0
- data/app/controllers/admin/bulk_actions_controller.rb +1 -1
- data/app/controllers/admin/document_accesses_controller.rb +3 -3
- data/app/controllers/admin/document_assets_controller.rb +33 -23
- data/app/controllers/admin/documents_controller.rb +6 -2
- data/app/controllers/admin/ids_controller.rb +0 -1
- data/app/controllers/admin/imports_controller.rb +2 -2
- data/app/helpers/asset_helper.rb +8 -0
- data/app/helpers/document_helper.rb +4 -0
- data/app/helpers/geoblacklight_admin_helper.rb +11 -1
- data/{lib/generators/geoblacklight_admin/templates → app}/javascript/controllers/results_controller.js +38 -0
- data/app/javascript/entrypoints/engine.js +8 -0
- data/app/javascript/index.js +8 -0
- data/app/jobs/bulk_action_revert_document_job.rb +2 -2
- data/app/jobs/bulk_action_run_document_job.rb +5 -1
- data/app/jobs/bulk_action_run_job.rb +11 -1
- data/app/jobs/geoblacklight_admin/delete_thumbnail_job.rb +17 -0
- data/app/jobs/geoblacklight_admin/remove_parent_dct_references_uri_job.rb +16 -0
- data/app/jobs/geoblacklight_admin/set_parent_dct_references_uri_job.rb +19 -0
- data/app/jobs/geoblacklight_admin/store_image_job.rb +21 -2
- data/app/models/asset.rb +38 -0
- data/app/models/blacklight_api.rb +2 -2
- data/app/models/blacklight_api_facets.rb +1 -1
- data/app/models/blacklight_api_ids.rb +2 -2
- data/app/models/bulk_action_document_state_machine.rb +2 -4
- data/app/models/bulk_action_state_machine.rb +3 -3
- data/app/models/bulk_actions/change_publication_state.rb +10 -0
- data/app/models/document/reference.rb +24 -0
- data/app/models/document.rb +122 -11
- data/app/models/document_thumbnail_state_machine.rb +22 -0
- data/app/models/document_thumbnail_transition.rb +26 -0
- data/app/models/element.rb +1 -1
- data/app/models/geoblacklight_admin/field_mappings_btaa_aardvark.rb +7 -1
- data/app/models/geoblacklight_admin/schema.rb +37 -1
- data/app/models/geoblacklight_admin/solr_utils.rb +87 -0
- data/app/models/kithe/vips_cli_image_to_png.rb +114 -0
- data/app/services/geoblacklight_admin/image_service/iiif.rb +2 -2
- data/app/services/geoblacklight_admin/image_service/iiif_manifest.rb +111 -0
- data/app/services/geoblacklight_admin/image_service/tms.rb +50 -0
- data/app/services/geoblacklight_admin/image_service/wms.rb +1 -4
- data/app/services/geoblacklight_admin/image_service.rb +16 -40
- data/app/services/geoblacklight_admin/item_viewer.rb +1 -1
- data/app/uploaders/asset_uploader.rb +6 -11
- data/app/views/admin/assets/_form.html.erb +19 -0
- data/app/views/admin/assets/display_attach_form.html.erb +39 -0
- data/app/views/admin/assets/edit.html.erb +9 -0
- data/app/views/admin/assets/index.html.erb +75 -0
- data/app/views/admin/assets/show.html.erb +100 -0
- data/app/views/admin/bulk_actions/index.html.erb +50 -48
- data/app/views/admin/bulk_actions/show.html.erb +3 -2
- data/app/views/admin/document_accesses/index.html.erb +68 -64
- data/app/views/admin/document_assets/_form.html.erb +17 -0
- data/app/views/admin/document_assets/display_attach_form.html.erb +4 -9
- data/app/views/admin/document_assets/edit.html.erb +5 -0
- data/app/views/admin/document_assets/index.html.erb +88 -72
- data/app/views/admin/document_downloads/index.html.erb +64 -62
- data/app/views/admin/documents/_document.html.erb +37 -16
- data/app/views/admin/documents/_form.html.erb +21 -6
- data/app/views/admin/documents/_form_nav.html.erb +12 -3
- data/app/views/admin/documents/_result_selected_options.html.erb +6 -1
- data/app/views/admin/documents/admin.html.erb +210 -0
- data/app/views/admin/documents/index.html.erb +10 -1
- data/app/views/admin/documents/versions.html.erb +3 -3
- data/app/views/admin/elements/index.html.erb +55 -54
- data/app/views/admin/form_elements/index.html.erb +38 -35
- data/app/views/admin/imports/index.html.erb +52 -50
- data/app/views/admin/layouts/application.html.erb +7 -4
- data/app/views/admin/shared/_js_behaviors.html.erb +6 -3
- data/app/views/admin/shared/_navbar.html.erb +11 -8
- data/config/locales/documents.en.yml +6 -0
- data/config/routes.rb +1 -0
- data/config/vite.json +14 -0
- data/db/migrate/20240619171628_create_document_thumbnail_statesman.rb +18 -0
- data/lib/generators/geoblacklight_admin/config_generator.rb +63 -15
- data/lib/generators/geoblacklight_admin/install_generator.rb +1 -0
- data/lib/generators/geoblacklight_admin/templates/api_controller.rb +0 -2
- data/lib/generators/geoblacklight_admin/templates/base.html.erb +53 -0
- data/lib/generators/geoblacklight_admin/templates/config/initializers/kithe.rb +1 -0
- data/lib/generators/geoblacklight_admin/templates/config/settings.yml +15 -1
- data/lib/generators/geoblacklight_admin/templates/config/vite.json +16 -0
- data/lib/generators/geoblacklight_admin/templates/frontend/entrypoints/application.js +30 -0
- data/lib/generators/geoblacklight_admin/templates/package-test.json +10 -0
- data/lib/generators/geoblacklight_admin/templates/package.json +5 -29
- data/lib/generators/geoblacklight_admin/templates/vite.config.ts +8 -0
- data/lib/geoblacklight_admin/engine.rb +1 -0
- data/lib/geoblacklight_admin/rake_task.rb +5 -0
- data/lib/geoblacklight_admin/tasks/images.rake +33 -0
- data/lib/geoblacklight_admin/tasks/solr.rake +11 -0
- data/lib/geoblacklight_admin/version.rb +1 -1
- metadata +75 -19
- data/lib/generators/geoblacklight_admin/templates/javascript/controllers/application_controller.js +0 -17
- data/lib/generators/geoblacklight_admin/templates/javascript/controllers/document_controller.js +0 -26
- data/lib/generators/geoblacklight_admin/templates/javascript/controllers/index.js +0 -10
- data/lib/tasks/geoblacklight_admin/images.rake +0 -30
- data/lib/tasks/geoblacklight_admin.rake +0 -213
|
@@ -164,6 +164,36 @@ module GeoblacklightAdmin
|
|
|
164
164
|
destination: solr_fields[:reference],
|
|
165
165
|
delimited: false,
|
|
166
166
|
transformation_method: "build_dct_references"
|
|
167
|
+
},
|
|
168
|
+
COG: {
|
|
169
|
+
destination: solr_fields[:reference],
|
|
170
|
+
delimited: false,
|
|
171
|
+
transformation_method: "build_dct_references"
|
|
172
|
+
},
|
|
173
|
+
PMTiles: {
|
|
174
|
+
destination: solr_fields[:reference],
|
|
175
|
+
delimited: false,
|
|
176
|
+
transformation_method: "build_dct_references"
|
|
177
|
+
},
|
|
178
|
+
"XYZ Tiles": {
|
|
179
|
+
destination: solr_fields[:reference],
|
|
180
|
+
delimited: false,
|
|
181
|
+
transformation_method: "build_dct_references"
|
|
182
|
+
},
|
|
183
|
+
WMTS: {
|
|
184
|
+
destination: solr_fields[:reference],
|
|
185
|
+
delimited: false,
|
|
186
|
+
transformation_method: "build_dct_references"
|
|
187
|
+
},
|
|
188
|
+
TileJSON: {
|
|
189
|
+
destination: solr_fields[:reference],
|
|
190
|
+
delimited: false,
|
|
191
|
+
transformation_method: "build_dct_references"
|
|
192
|
+
},
|
|
193
|
+
"Tile Map Service": {
|
|
194
|
+
destination: solr_fields[:reference],
|
|
195
|
+
delimited: false,
|
|
196
|
+
transformation_method: "build_dct_references"
|
|
167
197
|
}
|
|
168
198
|
}
|
|
169
199
|
end
|
|
@@ -190,7 +220,13 @@ module GeoblacklightAdmin
|
|
|
190
220
|
WCS: "wcs",
|
|
191
221
|
oEmbed: "oembed",
|
|
192
222
|
Thumbnail: "thumbnail",
|
|
193
|
-
Image: "image"
|
|
223
|
+
Image: "image",
|
|
224
|
+
COG: "cog",
|
|
225
|
+
PMTiles: "pmtiles",
|
|
226
|
+
"XYZ Tiles": "xyz_tiles",
|
|
227
|
+
WMTS: "wmts",
|
|
228
|
+
TileJSON: "tile_json",
|
|
229
|
+
"Tile Map Service": "tile_map_service"
|
|
194
230
|
}
|
|
195
231
|
end
|
|
196
232
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "rsolr"
|
|
3
|
+
|
|
4
|
+
module GeoblacklightAdmin
|
|
5
|
+
# @CUSOMIZATION
|
|
6
|
+
# This is mostly a copy of the Kithe::SolrUtil module from the Kithe gem
|
|
7
|
+
# with the exception changing id to geomg_id_s
|
|
8
|
+
|
|
9
|
+
# This is all somewhat hacky code, but it gets the job done. Some convenience utilities for dealing
|
|
10
|
+
# with your Solr index, including issuing a query to delete_all; and finding and deleting "orphaned"
|
|
11
|
+
# Kithe::Indexable Solr objects that no longer exist in the rdbms.
|
|
12
|
+
#
|
|
13
|
+
# Unlike other parts of Kithe's indexing support, this stuff IS very solr-specific, and generally
|
|
14
|
+
# implemented with [rsolr](https://github.com/rsolr/rsolr).
|
|
15
|
+
module SolrUtils
|
|
16
|
+
# based on sunspot, does not depend on Blacklight.
|
|
17
|
+
# https://github.com/sunspot/sunspot/blob/3328212da79178319e98699d408f14513855d3c0/sunspot_rails/lib/sunspot/rails/searchable.rb#L332
|
|
18
|
+
#
|
|
19
|
+
# solr_index_orphans do |orphaned_id|
|
|
20
|
+
# delete(id)
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# It is searching for any Solr object with a `Kithe.indexable_settings.model_name_solr_field`
|
|
24
|
+
# field (default `model_name_ssi`). Then, it takes the ID and makes sure it exists in
|
|
25
|
+
# the database using Kithe::Model. At the moment we are assuming everything is in Kithe::Model,
|
|
26
|
+
# rather than trying to use the `model_name_ssi` to fetch from different tables. Could
|
|
27
|
+
# maybe be enhanced to not.
|
|
28
|
+
#
|
|
29
|
+
# This is intended mostly for use by .delete_solr_orphans
|
|
30
|
+
#
|
|
31
|
+
# A bit hacky implementation, it might be nice to support a progress bar, we
|
|
32
|
+
# don't now.
|
|
33
|
+
def self.solr_orphan_geomg_ids(batch_size: 100, solr_url: Kithe.indexable_settings.solr_url)
|
|
34
|
+
return enum_for(:solr_index_orphan_ids) unless block_given?
|
|
35
|
+
|
|
36
|
+
model_solr_id_attr = Kithe.indexable_settings.solr_id_value_attribute
|
|
37
|
+
|
|
38
|
+
solr_page = -1
|
|
39
|
+
|
|
40
|
+
rsolr = RSolr.connect url: solr_url
|
|
41
|
+
|
|
42
|
+
while (solr_page = solr_page.next)
|
|
43
|
+
response = rsolr.get "select", params: {
|
|
44
|
+
rows: batch_size,
|
|
45
|
+
start: (batch_size * solr_page),
|
|
46
|
+
fl: "geomg_id_s",
|
|
47
|
+
q: "* TO *"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
solr_geomg_ids = response["response"]["docs"].collect { |h| h["geomg_id_s"] }
|
|
51
|
+
|
|
52
|
+
break if solr_geomg_ids.empty?
|
|
53
|
+
|
|
54
|
+
(solr_geomg_ids - Kithe::Model.where(model_solr_id_attr => solr_geomg_ids).pluck(model_solr_id_attr)).each do |orphaned_geomg_id|
|
|
55
|
+
yield orphaned_geomg_id
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Finds any Solr objects that have a `model_name_ssi` field
|
|
61
|
+
# (or `Kithe.indexable_settings.model_name_solr_field` if non-default), but don't
|
|
62
|
+
# exist in the rdbms, and deletes them from Solr, then issues a commit.
|
|
63
|
+
#
|
|
64
|
+
# Under normal use, you shouldn't have to do this, but can if your Solr index
|
|
65
|
+
# has gotten out of sync and you don't want to delete it and reindex from
|
|
66
|
+
# scratch.
|
|
67
|
+
#
|
|
68
|
+
# Implemented in terms of .solr_orphan_ids.
|
|
69
|
+
#
|
|
70
|
+
# A bit hacky implementation, it might be nice to have a progress bar, we don't now.
|
|
71
|
+
#
|
|
72
|
+
# Does return an array of any IDs deleted.
|
|
73
|
+
def self.delete_solr_orphans(batch_size: 100, solr_url: Kithe.indexable_settings.solr_url)
|
|
74
|
+
rsolr = RSolr.connect url: solr_url
|
|
75
|
+
deleted_geomg_ids = []
|
|
76
|
+
|
|
77
|
+
solr_orphan_geomg_ids(batch_size: batch_size, solr_url: solr_url) do |orphan_geomg_id|
|
|
78
|
+
deleted_geomg_ids << orphan_geomg_id
|
|
79
|
+
rsolr.delete_by_query("geomg_id_s:#{orphan_geomg_id}")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
rsolr.commit
|
|
83
|
+
|
|
84
|
+
deleted_geomg_ids
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
require "tempfile"
|
|
2
|
+
require "tty/command"
|
|
3
|
+
|
|
4
|
+
module Kithe
|
|
5
|
+
# Use the [vips](https://jcupitt.github.io/libvips/) command-line utility (via shell-out)
|
|
6
|
+
# to transform any image type to a PNG, with a specified maximum width (keeping aspect ratio).
|
|
7
|
+
#
|
|
8
|
+
# Requires vips command line utilities `vips` and `vipsthumbnail` and to be installed on your system,
|
|
9
|
+
# eg `brew install vips`, or apt package `vips-tools`.
|
|
10
|
+
#
|
|
11
|
+
# If thumbnail_mode:true is given, we ALSO apply some additional best practices
|
|
12
|
+
# for minimizing size when used as an image _in a browser_, such as removing
|
|
13
|
+
# color profile information. See eg:
|
|
14
|
+
# * https://developers.google.com/speed/docs/insights/OptimizeImages
|
|
15
|
+
# * http://libvips.blogspot.com/2013/11/tips-and-tricks-for-vipsthumbnail.html
|
|
16
|
+
# * https://github.com/jcupitt/libvips/issues/775
|
|
17
|
+
#
|
|
18
|
+
# It takes an open `File` object in, and returns an open TempFile object. It is
|
|
19
|
+
# built for use with kithe derivatives transformations, eg:
|
|
20
|
+
#
|
|
21
|
+
# class Asset < KitheAsset
|
|
22
|
+
# define_derivative(thumb) do |original_file|
|
|
23
|
+
# Kithe::VipsCliImageToPng.new(max_width: 100, thumbnail_mode: true).call(original_file)
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# We use the vips CLI because we know how, and it means we can avoid worrying
|
|
28
|
+
# about ruby memory leaks or the GIL. An alternative that uses vips ruby bindings
|
|
29
|
+
# would also be possible, and might work well, but this is what for us is tried
|
|
30
|
+
# and true.
|
|
31
|
+
class VipsCliImageToPng
|
|
32
|
+
class_attribute :srgb_profile_path, default: Kithe::Engine.root.join("lib", "vendor", "icc", "sRGB2014.icc").to_s
|
|
33
|
+
class_attribute :vips_thumbnail_command, default: "vipsthumbnail"
|
|
34
|
+
class_attribute :vips_command, default: "vips"
|
|
35
|
+
|
|
36
|
+
attr_reader :max_width, :png_compression
|
|
37
|
+
|
|
38
|
+
def initialize(max_width: nil, png_compression: 1, thumbnail_mode: false)
|
|
39
|
+
@max_width = max_width
|
|
40
|
+
@png_compression = png_compression
|
|
41
|
+
@thumbnail_mode = !!thumbnail_mode
|
|
42
|
+
|
|
43
|
+
if thumbnail_mode && max_width.nil?
|
|
44
|
+
# https://github.com/libvips/libvips/issues/1179
|
|
45
|
+
raise ArgumentError.new("thumbnail_mode currently requires a non-nil max_width")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Will raise TTY::Command::ExitError if the external Vips command returns non-null.
|
|
50
|
+
def call(original_file)
|
|
51
|
+
tempfile = Tempfile.new(["kithe_vips_cli_image_to_png", ".png"])
|
|
52
|
+
|
|
53
|
+
vips_args = []
|
|
54
|
+
|
|
55
|
+
# If we are resizing, we use `vipsthumbnail`, if we are not resizing,
|
|
56
|
+
# `vips copy` works better.
|
|
57
|
+
if max_width
|
|
58
|
+
# Due to bug in vips, we need to provide a height constraint, we make
|
|
59
|
+
# really huge one million pixels so it should not come into play, and
|
|
60
|
+
# we're constraining proportionally by width.
|
|
61
|
+
# https://github.com/jcupitt/libvips/issues/781
|
|
62
|
+
vips_args.concat [vips_thumbnail_command, original_file.path]
|
|
63
|
+
vips_args.concat maybe_profile_normalization_args
|
|
64
|
+
vips_args.concat ["--size", "#{max_width}x65500"]
|
|
65
|
+
vips_args.concat ["-o", "#{tempfile.path}#{vips_png_params}"]
|
|
66
|
+
else
|
|
67
|
+
# If we arne't making a thumbnail, we need to use `vips copy` instead of `vipsthumbnail`,
|
|
68
|
+
# to avoid it changing height/width on us. There might be another way.
|
|
69
|
+
#
|
|
70
|
+
# Yes, this means we can't do thumbnail-mode normalizations.
|
|
71
|
+
vips_args.concat [vips_command, "copy", original_file.path]
|
|
72
|
+
vips_args.concat ["#{tempfile.path}#{vips_png_params}"]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
TTY::Command.new(printer: :null).run(*vips_args)
|
|
76
|
+
|
|
77
|
+
tempfile
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def thumbnail_mode?
|
|
83
|
+
@thumbnail_mode
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Only if we're in thumbnail_mode mode, normalize to rRGB profile, and then strip
|
|
87
|
+
# embedded profile info for a smaller size, since browsers assume sRGB
|
|
88
|
+
def maybe_profile_normalization_args
|
|
89
|
+
return [] unless thumbnail_mode?
|
|
90
|
+
|
|
91
|
+
["--eprofile", srgb_profile_path, "--delete"]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Params to add on to end of PNG output path, as in:
|
|
95
|
+
# `vips convert ... -o something.png[Q=85]`
|
|
96
|
+
#
|
|
97
|
+
# If we are in thumbnail mode, we strip all profile information for
|
|
98
|
+
# smaller files.
|
|
99
|
+
#
|
|
100
|
+
# Either way we create an interlaced JPG and optimize coding for smaller
|
|
101
|
+
# file size.
|
|
102
|
+
#
|
|
103
|
+
# @returns [String]
|
|
104
|
+
def vips_png_params
|
|
105
|
+
if thumbnail_mode?
|
|
106
|
+
"[compression=#{png_compression},interlace,strip]"
|
|
107
|
+
else
|
|
108
|
+
# could be higher Q for downloads if we want, but we don't right now
|
|
109
|
+
# We do avoid striping metadata, no 'strip' directive.
|
|
110
|
+
"[compression=#{png_compression},interlace]"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -9,8 +9,8 @@ module GeoblacklightAdmin
|
|
|
9
9
|
# @param [SolrDocument]
|
|
10
10
|
# @param [Integer] thumbnail size
|
|
11
11
|
# @return [String] iiif thumbnail url
|
|
12
|
-
def self.image_url(document,
|
|
13
|
-
"#{document.viewer_endpoint.gsub("info.json", "")}full
|
|
12
|
+
def self.image_url(document, _size)
|
|
13
|
+
"#{document.viewer_endpoint.gsub("info.json", "")}full/max/0/default.jpg"
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GeoblacklightAdmin
|
|
4
|
+
class ImageService
|
|
5
|
+
module IiifManifest
|
|
6
|
+
require "down"
|
|
7
|
+
require "json"
|
|
8
|
+
|
|
9
|
+
## IIIF Manifests are messy
|
|
10
|
+
# - some have a sequence
|
|
11
|
+
# - some have items
|
|
12
|
+
# - some have canvases
|
|
13
|
+
# - some have images
|
|
14
|
+
# - some have resources
|
|
15
|
+
# - some have thumbnails
|
|
16
|
+
|
|
17
|
+
## BTAA Examples
|
|
18
|
+
# Indiana - No IIIF Manifests (that I know of)
|
|
19
|
+
#
|
|
20
|
+
# Illinois - IIIF Manifests have thumbnail entries
|
|
21
|
+
# ex. https://digital.library.illinois.edu/items/a07f9c70-994e-0134-2096-0050569601ca-8/manifest
|
|
22
|
+
#
|
|
23
|
+
# Iowa - No IIIF Manifests (that I know of)
|
|
24
|
+
#
|
|
25
|
+
# Maryland - No IIIF Manifests (that I know of)
|
|
26
|
+
#
|
|
27
|
+
# Michigan - IIIF Manifests have thumbnail entries
|
|
28
|
+
# ex. https://quod.lib.umich.edu/cgi/i/image/api/search/clark1ic:003287878
|
|
29
|
+
#
|
|
30
|
+
# Michigan State -
|
|
31
|
+
# ex. https://d.lib.msu.edu/maps/1084/manifest
|
|
32
|
+
#
|
|
33
|
+
# Minnesota
|
|
34
|
+
# ex. https://cdm16022.contentdm.oclc.org/iiif/info/p16022coll205/265/manifest.json
|
|
35
|
+
#
|
|
36
|
+
# Nebraska -
|
|
37
|
+
# ex. https://mediacommons.unl.edu/luna/servlet/iiif/m/RUMSEY~8~1~317895~90086920/manifest
|
|
38
|
+
#
|
|
39
|
+
# Northwestern -
|
|
40
|
+
# ex. https://api.dc.library.northwestern.edu/api/v2/works/183249d3-aff6-40d5-b445-8f70addedcc3?as=iiif
|
|
41
|
+
#
|
|
42
|
+
# Ohio State - IIIF Manifests do not have thumbnail entries
|
|
43
|
+
# ex. https://library.osu.edu/dc/concern/generic_works/9k41zr126/manifest
|
|
44
|
+
#
|
|
45
|
+
# Penn State -
|
|
46
|
+
# ex. https://digital.libraries.psu.edu/iiif/2/maps1:30183/manifest.json
|
|
47
|
+
#
|
|
48
|
+
# Rutgers - No IIIF Manifests (that I know of)
|
|
49
|
+
#
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# Formats and returns a thumbnail url from a IIIF Manifest
|
|
53
|
+
# @param [SolrDocument]
|
|
54
|
+
# @param [Integer] thumbnail size
|
|
55
|
+
# @return [String] iiif thumbnail url
|
|
56
|
+
def self.image_url(document, _size)
|
|
57
|
+
Rails.logger.debug("\n\nViewer Endpoint: #{document.viewer_endpoint}")
|
|
58
|
+
|
|
59
|
+
begin
|
|
60
|
+
tempfile = Down.download(document.viewer_endpoint)
|
|
61
|
+
manifest_json = JSON.parse(tempfile.read)
|
|
62
|
+
|
|
63
|
+
# Sequences - Return the first image if it exists
|
|
64
|
+
# - best case option
|
|
65
|
+
if manifest_json.dig("sequences", 0, "canvases", 0, "images", 0, "resource", "@id")
|
|
66
|
+
Rails.logger.debug("\n Image: sequences \n")
|
|
67
|
+
if manifest_json.dig("sequences", 0, "canvases", 0, "images", 0, "resource", "@id").include?("osu")
|
|
68
|
+
Rails.logger.debug("\n Image: sequences - OSU variant \n")
|
|
69
|
+
manifest_json.dig("sequences", 0, "canvases", 0, "images", 0, "resource", "service", "@id") + "/full/1000,/0/default.jpg"
|
|
70
|
+
else
|
|
71
|
+
manifest_json.dig("sequences", 0, "canvases", 0, "images", 0, "resource", "@id")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Items - Return the first item image if it exists
|
|
75
|
+
# - Northwestern
|
|
76
|
+
elsif manifest_json.dig("items", 0, "items", 0, "items", 0, "body", "id")
|
|
77
|
+
Rails.logger.debug("\n Image: items body id \n")
|
|
78
|
+
manifest_json.dig("items", 0, "items", 0, "items", 0, "body", "id")
|
|
79
|
+
|
|
80
|
+
# Items - Return the first item image if it exists
|
|
81
|
+
# - strange option
|
|
82
|
+
elsif manifest_json.dig("items", 0, "items", 0, "items", 0, "id")
|
|
83
|
+
Rails.logger.debug("\n Image: items id \n")
|
|
84
|
+
manifest_json.dig("items", 0, "items", 0, "items", 0, "id")
|
|
85
|
+
|
|
86
|
+
# Thumbnail - Return the "thumbnail" if it exists
|
|
87
|
+
# - varies in size depending on the provider
|
|
88
|
+
# - worst case option really
|
|
89
|
+
# - can be @id or id
|
|
90
|
+
elsif manifest_json["thumbnail"]
|
|
91
|
+
Rails.logger.debug("\n Image: thumbnail \n")
|
|
92
|
+
if manifest_json.dig("thumbnail", "@id")
|
|
93
|
+
manifest_json.dig("thumbnail", "@id")
|
|
94
|
+
else
|
|
95
|
+
manifest_json.dig("thumbnail", "id")
|
|
96
|
+
manifest_json.dig("thumbnail", "id")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Fail - Gonna fail
|
|
100
|
+
else
|
|
101
|
+
Rails.logger.debug("\n Image: failed \n")
|
|
102
|
+
document.viewer_endpoint
|
|
103
|
+
end
|
|
104
|
+
rescue => e
|
|
105
|
+
Rails.logger.debug("\n Rescued: #{e.inspect} \n")
|
|
106
|
+
document.viewer_endpoint
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "addressable/uri"
|
|
4
|
+
|
|
5
|
+
module GeoblacklightAdmin
|
|
6
|
+
class ImageService
|
|
7
|
+
module Tms
|
|
8
|
+
##
|
|
9
|
+
# Formats and returns a thumbnail url for a TMS endpoint from a Web Map Service.
|
|
10
|
+
# This utilizes the GeoServer specific 'reflect' service to generate
|
|
11
|
+
# parameters like bbox that are difficult to tweak without more detailed
|
|
12
|
+
# information about the layer.
|
|
13
|
+
# @param [SolrDocument]
|
|
14
|
+
# @param [Integer] thumbnail size
|
|
15
|
+
# @return [String] tms thumbnail url
|
|
16
|
+
def self.image_url(document, size)
|
|
17
|
+
puts "\nTMS IMAGE URL..."
|
|
18
|
+
puts "document.viewer_endpoint: #{document.viewer_endpoint.inspect}"
|
|
19
|
+
|
|
20
|
+
# Begins with:
|
|
21
|
+
# https://cugir.library.cornell.edu/geoserver/gwc/service/tms/1.0.0/cugir%3Acugir007957@EPSG%3A3857@png/{z}/{x}/{y}.png
|
|
22
|
+
|
|
23
|
+
# Works with:
|
|
24
|
+
# https://cugir.library.cornell.edu/geoserver/wms/reflect?&FORMAT=image%2Fpng&TRANSPARENT=TRUE&LAYERS=cugir007957&WIDTH=1500&HEIGHT=1500
|
|
25
|
+
|
|
26
|
+
puts "\nPARSE TMS URL..."
|
|
27
|
+
# Parse the URL using Addressable::URI which handles more complex URIs
|
|
28
|
+
parsed_url = Addressable::URI.parse(document.viewer_endpoint)
|
|
29
|
+
|
|
30
|
+
puts "Parsed URL: #{parsed_url.inspect}"
|
|
31
|
+
|
|
32
|
+
# Build a hash to store the extracted components
|
|
33
|
+
parsed_data = {
|
|
34
|
+
base_url: "#{parsed_url.scheme}://#{parsed_url.host}#{parsed_url.port ? ":" + parsed_url.port.to_s : ""}",
|
|
35
|
+
path_pattern: parsed_url.path
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
puts "Parsed Data: #{parsed_data.inspect}"
|
|
39
|
+
|
|
40
|
+
endpoint = parsed_data[:base_url]
|
|
41
|
+
"#{endpoint}/geoserver/wms/reflect?" \
|
|
42
|
+
"&FORMAT=image%2Fpng" \
|
|
43
|
+
"&TRANSPARENT=TRUE" \
|
|
44
|
+
"&LAYERS=#{document["gbl_wxsIdentifier_s"]}" \
|
|
45
|
+
"&WIDTH=#{size}" \
|
|
46
|
+
"&HEIGHT=#{size}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -12,10 +12,7 @@ module GeoblacklightAdmin
|
|
|
12
12
|
# @param [Integer] thumbnail size
|
|
13
13
|
# @return [String] wms thumbnail url
|
|
14
14
|
def self.image_url(document, size)
|
|
15
|
-
|
|
16
|
-
# Thumbnail requests send geoserver auth.
|
|
17
|
-
endpoint = document.viewer_endpoint.gsub(Settings.PROXY_GEOSERVER_URL,
|
|
18
|
-
Settings.INSTITUTION_GEOSERVER_URL)
|
|
15
|
+
endpoint = document.viewer_endpoint
|
|
19
16
|
"#{endpoint}/reflect?" \
|
|
20
17
|
"&FORMAT=image%2Fpng" \
|
|
21
18
|
"&TRANSPARENT=TRUE" \
|
|
@@ -10,10 +10,13 @@ module GeoblacklightAdmin
|
|
|
10
10
|
def initialize(document)
|
|
11
11
|
@document = document
|
|
12
12
|
|
|
13
|
+
# State Machine
|
|
13
14
|
@metadata = {}
|
|
14
15
|
@metadata["solr_doc_id"] = document.id
|
|
15
16
|
@metadata["placeheld"] = false
|
|
17
|
+
@document.thumbnail_state_machine.transition_to!(:processing, @metadata)
|
|
16
18
|
|
|
19
|
+
# Logger
|
|
17
20
|
@logger ||= ActiveSupport::TaggedLogging.new(
|
|
18
21
|
Logger.new(
|
|
19
22
|
Rails.root.join("log", "image_service_#{Rails.env}.log")
|
|
@@ -28,37 +31,27 @@ module GeoblacklightAdmin
|
|
|
28
31
|
# Gentle hands
|
|
29
32
|
sleep(1)
|
|
30
33
|
|
|
31
|
-
puts "Storing ImageService..."
|
|
32
|
-
puts "Document ID: #{@document.id}"
|
|
33
|
-
|
|
34
34
|
io_file = image_tempfile(@document.id)
|
|
35
35
|
|
|
36
|
-
if io_file.nil?
|
|
37
|
-
|
|
36
|
+
if io_file.nil? || @metadata["placeheld"] == true
|
|
37
|
+
@metadata["IO"] = "NIL"
|
|
38
|
+
@document.thumbnail_state_machine.transition_to!(:placeheld, @metadata)
|
|
38
39
|
else
|
|
39
|
-
puts "Attaching IO"
|
|
40
40
|
attach_io(io_file)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
log_output
|
|
44
44
|
rescue => e
|
|
45
45
|
@metadata["exception"] = e.inspect
|
|
46
|
+
@document.thumbnail_state_machine.transition_to!(:failed, @metadata)
|
|
46
47
|
log_output
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
private
|
|
50
51
|
|
|
51
52
|
def image_tempfile(document_id)
|
|
52
|
-
# puts "IMAGE TEMPFILE..."
|
|
53
|
-
puts "Document Viewer Protocol: #{@document.viewer_protocol}"
|
|
54
|
-
# puts "Image URL: #{image_url}"
|
|
55
|
-
# puts "IMAGE DATA: #{image_data}"
|
|
56
|
-
|
|
57
53
|
@metadata["viewer_protocol"] = @document.viewer_protocol
|
|
58
54
|
@metadata["image_url"] = image_url
|
|
59
|
-
@metadata["gblsi_thumbnail_uri"] = gblsi_thumbnail_uri
|
|
60
|
-
|
|
61
|
-
# puts "IMAGE DATA: #{image_data.inspect}"
|
|
62
55
|
|
|
63
56
|
return nil unless image_data && @metadata["placeheld"] == false
|
|
64
57
|
|
|
@@ -67,24 +60,18 @@ module GeoblacklightAdmin
|
|
|
67
60
|
temp_file.write(image_data)
|
|
68
61
|
temp_file.rewind
|
|
69
62
|
|
|
70
|
-
# puts "TEMPFILE: #{temp_file.inspect}"
|
|
71
|
-
|
|
72
63
|
@metadata["image_tempfile"] = temp_file.inspect
|
|
73
64
|
temp_file
|
|
74
65
|
end
|
|
75
66
|
|
|
76
67
|
def attach_io(io)
|
|
77
|
-
|
|
78
|
-
# Pull the mimetype and file extension via MimeMagic
|
|
79
|
-
content_type = Marcel::MimeType.for(File.open(io))
|
|
80
|
-
mime_type = Marcel::TYPES[content_type][0][0]
|
|
68
|
+
@document.document_assets.where("json_attributes->>'thumbnail' = ?", "true").destroy_all
|
|
81
69
|
|
|
82
|
-
|
|
83
|
-
|
|
70
|
+
content_type = Marcel::MimeType.for(File.open(io))
|
|
71
|
+
@metadata["content_type"] = content_type.inspect
|
|
84
72
|
|
|
85
73
|
if content_type.start_with?("image")
|
|
86
|
-
|
|
87
|
-
puts "\n\nStoring an image!\n\n"
|
|
74
|
+
@metadata["storing_image"] = io.inspect
|
|
88
75
|
|
|
89
76
|
asset = Asset.new
|
|
90
77
|
asset.parent_id = @document.id
|
|
@@ -92,8 +79,10 @@ module GeoblacklightAdmin
|
|
|
92
79
|
asset.title = (asset.file&.original_filename || "Untitled")
|
|
93
80
|
asset.thumbnail = true
|
|
94
81
|
asset.save
|
|
82
|
+
|
|
83
|
+
@document.thumbnail_state_machine.transition_to!(:succeeded, @metadata)
|
|
95
84
|
else
|
|
96
|
-
|
|
85
|
+
@document.thumbnail_state_machine.transition_to!(:placeheld, @metadata)
|
|
97
86
|
end
|
|
98
87
|
end
|
|
99
88
|
|
|
@@ -119,7 +108,7 @@ module GeoblacklightAdmin
|
|
|
119
108
|
end
|
|
120
109
|
|
|
121
110
|
def gblsi_thumbnail_uri
|
|
122
|
-
if gblsi_thumbnail_field? && @document.send(Settings.GBLSI_THUMBNAIL_FIELD)
|
|
111
|
+
if gblsi_thumbnail_field? && @document.send(Settings.GBLSI_THUMBNAIL_FIELD).present?
|
|
123
112
|
@document.send(Settings.GBLSI_THUMBNAIL_FIELD)
|
|
124
113
|
else
|
|
125
114
|
false
|
|
@@ -128,7 +117,6 @@ module GeoblacklightAdmin
|
|
|
128
117
|
|
|
129
118
|
# Generates hash containing thumbnail mime_type and image.
|
|
130
119
|
def image_data
|
|
131
|
-
# puts "IMAGE DATA..."
|
|
132
120
|
return nil unless image_url
|
|
133
121
|
|
|
134
122
|
remote_image
|
|
@@ -136,7 +124,6 @@ module GeoblacklightAdmin
|
|
|
136
124
|
|
|
137
125
|
# Gets thumbnail image from URL. On error, placehold image.
|
|
138
126
|
def remote_image
|
|
139
|
-
# puts "remote_image..."
|
|
140
127
|
auth = geoserver_credentials
|
|
141
128
|
|
|
142
129
|
uri = Addressable::URI.parse(image_url)
|
|
@@ -166,12 +153,6 @@ module GeoblacklightAdmin
|
|
|
166
153
|
# have not been set beyond the default, then a thumbnail url from
|
|
167
154
|
# dct references is used instead.
|
|
168
155
|
def image_url
|
|
169
|
-
# puts "IMAGE URL..."
|
|
170
|
-
# puts "gblsi_thumbnail_uri: #{gblsi_thumbnail_uri.inspect}"
|
|
171
|
-
# puts "restricted_scanned_map?: #{restricted_scanned_map?}"
|
|
172
|
-
# puts "service_url: #{service_url.inspect}"
|
|
173
|
-
# puts "image_reference: #{image_reference.inspect}"
|
|
174
|
-
|
|
175
156
|
@image_url ||= gblsi_thumbnail_uri || service_url || image_reference
|
|
176
157
|
end
|
|
177
158
|
|
|
@@ -186,14 +167,12 @@ module GeoblacklightAdmin
|
|
|
186
167
|
# from the viewer protocol, and if it's loaded, the image_url
|
|
187
168
|
# method is called.
|
|
188
169
|
def service_url
|
|
189
|
-
# puts "SERVICE URL..."
|
|
190
170
|
# Follow image_url instead
|
|
191
|
-
return nil if gblsi_thumbnail_uri
|
|
171
|
+
return nil if gblsi_thumbnail_uri.present?
|
|
192
172
|
|
|
193
173
|
@service_url ||=
|
|
194
174
|
begin
|
|
195
175
|
return unless @document.available?
|
|
196
|
-
|
|
197
176
|
protocol = @document.viewer_protocol
|
|
198
177
|
|
|
199
178
|
if protocol == "map" || protocol.nil?
|
|
@@ -202,8 +181,6 @@ module GeoblacklightAdmin
|
|
|
202
181
|
return nil
|
|
203
182
|
end
|
|
204
183
|
|
|
205
|
-
puts "Image Service: #{protocol.to_s.camelcase}"
|
|
206
|
-
|
|
207
184
|
"GeoblacklightAdmin::ImageService::#{protocol.to_s.camelcase}".constantize.image_url(@document, image_size)
|
|
208
185
|
rescue NameError
|
|
209
186
|
@metadata["error"] = "service_url NameError"
|
|
@@ -229,7 +206,6 @@ module GeoblacklightAdmin
|
|
|
229
206
|
|
|
230
207
|
# Capture metadata within image harvest log
|
|
231
208
|
def log_output
|
|
232
|
-
# @metadata["state"] = @document.sidecar.image_state.current_state
|
|
233
209
|
@metadata.each do |key, value|
|
|
234
210
|
@logger.tagged(@document.id, key.to_s) { @logger.info value }
|
|
235
211
|
end
|
|
@@ -24,7 +24,7 @@ module GeoblacklightAdmin
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def viewer_preference
|
|
27
|
-
[:oembed, :index_map, :tilejson, :xyz, :wmts, :tms, :wms, :iiif, :tiled_map_layer, :dynamic_map_layer,
|
|
27
|
+
[:cog, :pmtiles, :oembed, :index_map, :tilejson, :xyz, :wmts, :tms, :wms, :iiif_manifest, :iiif, :tiled_map_layer, :dynamic_map_layer,
|
|
28
28
|
:image_map_layer, :feature_layer]
|
|
29
29
|
end
|
|
30
30
|
end
|
|
@@ -1,31 +1,26 @@
|
|
|
1
1
|
class AssetUploader < Kithe::AssetUploader
|
|
2
|
-
# gives us md5, sha1, sha512
|
|
3
2
|
plugin :kithe_checksum_signatures
|
|
4
3
|
|
|
5
|
-
THUMB_WIDTHS =
|
|
6
|
-
mini: 54,
|
|
7
|
-
large: 525,
|
|
8
|
-
standard: 208
|
|
9
|
-
}
|
|
4
|
+
THUMB_WIDTHS = Settings.GBL_ADMIN_THUMBNAIL_WIDTHS
|
|
10
5
|
|
|
11
|
-
#
|
|
6
|
+
# Define thumb derivatives for image input: :thumb_mini, :thumb_mini_2X, etc.
|
|
12
7
|
THUMB_WIDTHS.each_pair do |key, width|
|
|
13
8
|
# Single-width thumbnails
|
|
14
9
|
Attacher.define_derivative("thumb_#{key}", content_type: "image") do |original_file|
|
|
15
|
-
Kithe::
|
|
10
|
+
Kithe::VipsCliImageToPng.new(max_width: width, thumbnail_mode: true).call(original_file)
|
|
16
11
|
end
|
|
17
12
|
|
|
18
13
|
# Double-width thumbnails
|
|
19
14
|
Attacher.define_derivative("thumb_#{key}_2X", content_type: "image") do |original_file|
|
|
20
|
-
Kithe::
|
|
15
|
+
Kithe::VipsCliImageToPng.new(max_width: width * 2, thumbnail_mode: true).call(original_file)
|
|
21
16
|
end
|
|
22
17
|
end
|
|
23
18
|
|
|
24
|
-
#
|
|
19
|
+
# And capture a full size jpg
|
|
25
20
|
Attacher.define_derivative("download_full", content_type: "image") do |original_file, attacher:|
|
|
26
21
|
# No need to do this if our original is a JPG
|
|
27
22
|
unless attacher.file.content_type == "image/jpeg"
|
|
28
|
-
Kithe::
|
|
23
|
+
Kithe::VipsCliImageToPng.new.call(original_file)
|
|
29
24
|
end
|
|
30
25
|
end
|
|
31
26
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<% if @asset.errors.any? %>
|
|
2
|
+
<div class="alert alert-danger mb-4" role="alert">
|
|
3
|
+
<h2 class="h4" class="alert-heading"><%= pluralize(@asset.errors.count, "error") %> prohibited this asset from being saved</h2>
|
|
4
|
+
<ol class="mb-0">
|
|
5
|
+
<% @asset.errors.full_messages.each do |msg| %>
|
|
6
|
+
<li><%= msg %></li>
|
|
7
|
+
<% end %>
|
|
8
|
+
</ol>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<div class="form-inputs">
|
|
13
|
+
<%= f.input :id, {disabled: true} %>
|
|
14
|
+
<%= f.input :parent_id %>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="form-actions">
|
|
18
|
+
<%= f.button :submit, {class: 'btn btn-primary'} %>
|
|
19
|
+
</div>
|