iiif_print 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (211) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +2 -0
  3. data/.env +5 -0
  4. data/.fcrepo_wrapper +4 -0
  5. data/.github/release.yml +20 -0
  6. data/.github/workflows/branches.yml +24 -0
  7. data/.github/workflows/build-lint-test-action.yaml +33 -0
  8. data/.github/workflows/release_labels.yml +25 -0
  9. data/.gitignore +52 -0
  10. data/.rubocop.yml +177 -0
  11. data/.solr_wrapper +8 -0
  12. data/.travis.yml +49 -0
  13. data/CONTRIBUTING.md +181 -0
  14. data/Dockerfile +15 -0
  15. data/Gemfile +52 -0
  16. data/LICENSE +203 -0
  17. data/README.md +203 -0
  18. data/Rakefile +38 -0
  19. data/app/actors/iiif_print/actors/file_set_actor_decorator.rb +56 -0
  20. data/app/assets/config/iiif_print_manifest.js +2 -0
  21. data/app/assets/images/iiif_print/.keep +0 -0
  22. data/app/assets/javascripts/iiif_print/autocomplete_fix.js +33 -0
  23. data/app/assets/javascripts/iiif_print/ocr_search.js.erb +6 -0
  24. data/app/assets/javascripts/iiif_print.js +3 -0
  25. data/app/assets/stylesheets/iiif_print/_iiif_print.scss +4 -0
  26. data/app/assets/stylesheets/iiif_print/_issue_search.scss +13 -0
  27. data/app/assets/stylesheets/iiif_print/_issues_calendar.scss +18 -0
  28. data/app/assets/stylesheets/iiif_print/_newspapers_search.scss +38 -0
  29. data/app/assets/stylesheets/iiif_print/_search_results.scss +6 -0
  30. data/app/helpers/hyrax/iiif_helper.rb +22 -0
  31. data/app/helpers/iiif_print/application_helper.rb +5 -0
  32. data/app/helpers/iiif_print_helper.rb +64 -0
  33. data/app/indexers/concerns/iiif_print/child_indexer.rb +34 -0
  34. data/app/indexers/concerns/iiif_print/file_set_indexer.rb +29 -0
  35. data/app/mailers/iiif_print/application_mailer.rb +8 -0
  36. data/app/models/concerns/iiif_print/set_child_flag.rb +29 -0
  37. data/app/models/concerns/iiif_print/solr/document.rb +47 -0
  38. data/app/models/iiif_print/application_record.rb +6 -0
  39. data/app/models/iiif_print/derivative_attachment.rb +8 -0
  40. data/app/models/iiif_print/iiif_search_response_decorator.rb +17 -0
  41. data/app/models/iiif_print/ingest_file_relation.rb +14 -0
  42. data/app/models/iiif_print/pending_relationship.rb +7 -0
  43. data/app/presenters/iiif_print/iiif_manifest_presenter_behavior.rb +10 -0
  44. data/app/presenters/iiif_print/iiif_manifest_presenter_factory_behavior.rb +33 -0
  45. data/app/presenters/iiif_print/work_show_presenter_decorator.rb +29 -0
  46. data/app/renderers/hyrax/renderers/faceted_attribute_renderer_decorator.rb +18 -0
  47. data/app/search_builders/concerns/iiif_print/exclude_models.rb +17 -0
  48. data/app/search_builders/concerns/iiif_print/highlight_search_params.rb +14 -0
  49. data/app/services/iiif_print/manifest_builder_service_behavior.rb +97 -0
  50. data/app/services/iiif_print/pluggable_derivative_service.rb +120 -0
  51. data/app/views/catalog/_snippets_more.html.erb +16 -0
  52. data/app/views/hyrax/base/_representative_media.html.erb +9 -0
  53. data/app/views/hyrax/base/iiif_viewers/_universal_viewer.html.erb +8 -0
  54. data/app/views/hyrax/file_sets/_actions.html.erb +45 -0
  55. data/bin/rails +13 -0
  56. data/config/fcrepo_wrapper_test.yml +5 -0
  57. data/config/initializers/assets.rb +2 -0
  58. data/config/locales/iiif_print.de.yml +148 -0
  59. data/config/locales/iiif_print.en.yml +119 -0
  60. data/config/locales/iiif_print.es.yml +148 -0
  61. data/config/locales/iiif_print.fr.yml +149 -0
  62. data/config/locales/iiif_print.it.yml +142 -0
  63. data/config/locales/iiif_print.pt-BR.yml +148 -0
  64. data/config/locales/iiif_print.zh.yml +142 -0
  65. data/config/solr_wrapper_test.yml +9 -0
  66. data/config/test-fixture/solr-config/_rest_managed.json +3 -0
  67. data/config/test-fixture/solr-config/admin-extra.html +31 -0
  68. data/config/test-fixture/solr-config/elevate.xml +36 -0
  69. data/config/test-fixture/solr-config/mapping-ISOLatin1Accent.txt +246 -0
  70. data/config/test-fixture/solr-config/protwords.txt +21 -0
  71. data/config/test-fixture/solr-config/schema.xml +366 -0
  72. data/config/test-fixture/solr-config/scripts.conf +24 -0
  73. data/config/test-fixture/solr-config/solrconfig.xml +322 -0
  74. data/config/test-fixture/solr-config/spellings.txt +2 -0
  75. data/config/test-fixture/solr-config/stopwords.txt +58 -0
  76. data/config/test-fixture/solr-config/stopwords_en.txt +58 -0
  77. data/config/test-fixture/solr-config/synonyms.txt +31 -0
  78. data/config/test-fixture/solr-config/xslt/example.xsl +132 -0
  79. data/config/test-fixture/solr-config/xslt/example_atom.xsl +67 -0
  80. data/config/test-fixture/solr-config/xslt/example_rss.xsl +66 -0
  81. data/config/test-fixture/solr-config/xslt/luke.xsl +337 -0
  82. data/config/vendor/fits.xml +55 -0
  83. data/config/vendor/imagemagick-6-policy.xml +76 -0
  84. data/db/migrate/20181214181358_create_iiif_print_derivative_attachments.rb +12 -0
  85. data/db/migrate/20190107165909_create_iiif_print_ingest_file_relations.rb +11 -0
  86. data/db/migrate/20230109000000_create_iiif_print_pending_relationships.rb +11 -0
  87. data/docker-compose.yml +129 -0
  88. data/iiif_print.gemspec +43 -0
  89. data/lib/generators/iiif_print/assets_generator.rb +29 -0
  90. data/lib/generators/iiif_print/catalog_controller_generator.rb +32 -0
  91. data/lib/generators/iiif_print/install_generator.rb +52 -0
  92. data/lib/generators/iiif_print/templates/config/initializers/iiif_print.rb +22 -0
  93. data/lib/generators/iiif_print/templates/iiif_print.scss +1 -0
  94. data/lib/iiif_print/base_derivative_service.rb +113 -0
  95. data/lib/iiif_print/blacklight_iiif_search/annotation_decorator.rb +84 -0
  96. data/lib/iiif_print/catalog_search_builder.rb +31 -0
  97. data/lib/iiif_print/configuration.rb +99 -0
  98. data/lib/iiif_print/data/fileset_helper.rb +25 -0
  99. data/lib/iiif_print/data/path_helper.rb +40 -0
  100. data/lib/iiif_print/data/work_derivatives.rb +323 -0
  101. data/lib/iiif_print/data/work_file.rb +92 -0
  102. data/lib/iiif_print/data/work_files.rb +199 -0
  103. data/lib/iiif_print/data.rb +35 -0
  104. data/lib/iiif_print/engine.rb +77 -0
  105. data/lib/iiif_print/errors.rb +9 -0
  106. data/lib/iiif_print/image_tool.rb +119 -0
  107. data/lib/iiif_print/jobs/application_job.rb +8 -0
  108. data/lib/iiif_print/jobs/child_works_from_pdf_job.rb +107 -0
  109. data/lib/iiif_print/jobs/create_relationships_job.rb +78 -0
  110. data/lib/iiif_print/jp2_derivative_service.rb +118 -0
  111. data/lib/iiif_print/jp2_image_metadata.rb +81 -0
  112. data/lib/iiif_print/lineage_service.rb +41 -0
  113. data/lib/iiif_print/metadata.rb +125 -0
  114. data/lib/iiif_print/pdf_derivative_service.rb +42 -0
  115. data/lib/iiif_print/split_pdfs/child_work_creation_from_pdf_service.rb +75 -0
  116. data/lib/iiif_print/split_pdfs/pages_into_images_service.rb +130 -0
  117. data/lib/iiif_print/split_pdfs/pdf_image_extraction_service.rb +85 -0
  118. data/lib/iiif_print/text_extraction/alto_reader.rb +123 -0
  119. data/lib/iiif_print/text_extraction/hocr_reader.rb +172 -0
  120. data/lib/iiif_print/text_extraction/page_ocr.rb +87 -0
  121. data/lib/iiif_print/text_extraction/render_alto.rb +84 -0
  122. data/lib/iiif_print/text_extraction/word_coords_builder.rb +38 -0
  123. data/lib/iiif_print/text_extraction.rb +11 -0
  124. data/lib/iiif_print/text_extraction_derivative_service.rb +47 -0
  125. data/lib/iiif_print/text_formats_from_alto_service.rb +77 -0
  126. data/lib/iiif_print/tiff_derivative_service.rb +50 -0
  127. data/lib/iiif_print/version.rb +3 -0
  128. data/lib/iiif_print/works_controller_behavior.rb +9 -0
  129. data/lib/iiif_print.rb +136 -0
  130. data/lib/tasks/set_child_works.rake +22 -0
  131. data/spec/.keep.txt +1 -0
  132. data/spec/factories/ability.rb +6 -0
  133. data/spec/factories/newspaper_issue.rb +7 -0
  134. data/spec/factories/newspaper_page.rb +7 -0
  135. data/spec/factories/newspaper_page_solr_document.rb +12 -0
  136. data/spec/factories/newspaper_title.rb +8 -0
  137. data/spec/factories/uploaded_pdf_file.rb +9 -0
  138. data/spec/factories/uploaded_txt_file.rb +9 -0
  139. data/spec/factories/user.rb +13 -0
  140. data/spec/fixtures/files/4.1.07.jp2 +0 -0
  141. data/spec/fixtures/files/4.1.07.tiff +0 -0
  142. data/spec/fixtures/files/README.md +7 -0
  143. data/spec/fixtures/files/alto-2-0.xsd +714 -0
  144. data/spec/fixtures/files/broken-truncated.pdf +0 -0
  145. data/spec/fixtures/files/credits.md +16 -0
  146. data/spec/fixtures/files/lowres-gray-via-ndnp-sample.tiff +0 -0
  147. data/spec/fixtures/files/minimal-1-page.pdf +0 -0
  148. data/spec/fixtures/files/minimal-2-page.pdf +0 -0
  149. data/spec/fixtures/files/minimal-alto.xml +31 -0
  150. data/spec/fixtures/files/ndnp-alto-sample.xml +24 -0
  151. data/spec/fixtures/files/ndnp-sample1-json.json +1 -0
  152. data/spec/fixtures/files/ndnp-sample1-txt.txt +1 -0
  153. data/spec/fixtures/files/ndnp-sample1.pdf +0 -0
  154. data/spec/fixtures/files/ocr_alto.xml +202 -0
  155. data/spec/fixtures/files/ocr_alto_scaled_4pts_per_px.xml +202 -0
  156. data/spec/fixtures/files/ocr_color.tiff +0 -0
  157. data/spec/fixtures/files/ocr_gray.jp2 +0 -0
  158. data/spec/fixtures/files/ocr_gray.tiff +0 -0
  159. data/spec/fixtures/files/ocr_mono.tiff +0 -0
  160. data/spec/fixtures/files/ocr_mono_text_hocr.html +78 -0
  161. data/spec/fixtures/files/page1.tiff +0 -0
  162. data/spec/fixtures/files/sample-4page-issue.pdf +0 -0
  163. data/spec/fixtures/files/sample-color-newsletter.pdf +0 -0
  164. data/spec/fixtures/files/thumbnail.jpg +0 -0
  165. data/spec/helpers/hyrax/iiif_helper_spec.rb +65 -0
  166. data/spec/helpers/iiif_print_helper_spec.rb +43 -0
  167. data/spec/iiif_print/base_derivative_service_spec.rb +11 -0
  168. data/spec/iiif_print/blacklight_iiif_search/annotation_decorator_spec.rb +51 -0
  169. data/spec/iiif_print/catalog_search_builder_spec.rb +60 -0
  170. data/spec/iiif_print/configuration_spec.rb +67 -0
  171. data/spec/iiif_print/data/work_derivatives_spec.rb +245 -0
  172. data/spec/iiif_print/data/work_file_spec.rb +99 -0
  173. data/spec/iiif_print/data/work_files_spec.rb +237 -0
  174. data/spec/iiif_print/image_tool_spec.rb +109 -0
  175. data/spec/iiif_print/jobs/child_works_from_pdf_job_spec.rb +30 -0
  176. data/spec/iiif_print/jobs/create_relationships_job_spec.rb +17 -0
  177. data/spec/iiif_print/jp2_image_metadata_spec.rb +37 -0
  178. data/spec/iiif_print/lineage_service_spec.rb +13 -0
  179. data/spec/iiif_print/metadata_spec.rb +115 -0
  180. data/spec/iiif_print/split_pdfs/pages_into_images_service_spec.rb +6 -0
  181. data/spec/iiif_print/text_extraction/alto_reader_spec.rb +49 -0
  182. data/spec/iiif_print/text_extraction/hocr_reader_spec.rb +45 -0
  183. data/spec/iiif_print/text_extraction/page_ocr_spec.rb +84 -0
  184. data/spec/iiif_print/text_extraction/render_alto_spec.rb +54 -0
  185. data/spec/iiif_print/text_extraction/word_coords_builder_spec.rb +44 -0
  186. data/spec/iiif_print_spec.rb +51 -0
  187. data/spec/misc_shared.rb +111 -0
  188. data/spec/models/iiif_print/derivative_attachment_spec.rb +37 -0
  189. data/spec/models/iiif_print/ingest_file_relation_spec.rb +56 -0
  190. data/spec/models/solr_document_spec.rb +14 -0
  191. data/spec/presenters/iiif_print/iiif_manifest_presenter_behavior_spec.rb +19 -0
  192. data/spec/presenters/iiif_print/iiif_manifest_presenter_factory_behavior_spec.rb +49 -0
  193. data/spec/services/iiif_print/jp2_derivative_service_spec.rb +59 -0
  194. data/spec/services/iiif_print/pdf_derivative_service_spec.rb +66 -0
  195. data/spec/services/iiif_print/pluggable_derivative_service_spec.rb +178 -0
  196. data/spec/services/iiif_print/text_extraction_derivative_service_spec.rb +82 -0
  197. data/spec/services/iiif_print/text_formats_from_alto_service_spec.rb +127 -0
  198. data/spec/services/iiif_print/tiff_derivative_service_spec.rb +65 -0
  199. data/spec/spec_helper.rb +181 -0
  200. data/spec/support/controller_level_helpers.rb +28 -0
  201. data/spec/support/iiif_print_models.rb +127 -0
  202. data/spec/test_app_templates/blacklight.yml +9 -0
  203. data/spec/test_app_templates/fedora.yml +15 -0
  204. data/spec/test_app_templates/lib/generators/test_app_generator.rb +40 -0
  205. data/spec/test_app_templates/redis.yml +9 -0
  206. data/spec/test_app_templates/solr/conf/schema.xml +362 -0
  207. data/spec/test_app_templates/solr/conf/solrconfig.xml +322 -0
  208. data/spec/test_app_templates/solr.yml +7 -0
  209. data/tasks/iiif_print_dev.rake +34 -0
  210. data/tmp/.keep +0 -0
  211. metadata +605 -0
@@ -0,0 +1,64 @@
1
+ module IiifPrintHelper
2
+ ##
3
+ # create link anchor to be read by UniversalViewer
4
+ # in order to show keyword search
5
+ # @param query_params_hash [Hash] current_search_session.query_params
6
+ # @return [String] or [nil] anchor
7
+ def iiif_search_anchor(query_params_hash)
8
+ query = search_query(query_params_hash)
9
+ return nil if query.blank?
10
+ "?h=#{query}"
11
+ end
12
+
13
+ ##
14
+ # get the query, which may be in a different object,
15
+ # depending if regular search or newspapers_search was run
16
+ # @param query_params_hash [Hash] current_search_session.query_params
17
+ # @return [String] or [nil] query
18
+ def search_query(query_params_hash)
19
+ query_params_hash[:q] || query_params_hash[:all_fields]
20
+ end
21
+
22
+ ##
23
+ # return the matching highlighted terms from Solr highlight field
24
+ #
25
+ # @param document [SolrDocument]
26
+ # @param hl_fl [String] the name of the Solr field with highlights
27
+ # @param hl_tag [String] the HTML element name used for marking highlights
28
+ # configured in Solr as hl.tag.pre value
29
+ # @return [String]
30
+ def highlight_matches(document, hl_fl, hl_tag)
31
+ hl_matches = []
32
+ # regex: find all chars between hl_tag, but NOT other <element>
33
+ regex = /<#{hl_tag}>[^<>]+<\/#{hl_tag}>/
34
+ hls = document.highlight_field(hl_fl)
35
+ return nil if hls.blank?
36
+ hls.each do |hl|
37
+ matches = hl.scan(regex)
38
+ matches.each do |match|
39
+ hl_matches << match.gsub(/<[\/]*#{hl_tag}>/, '').downcase
40
+ end
41
+ end
42
+ hl_matches.uniq.sort.join(' ')
43
+ end
44
+
45
+ ##
46
+ # print the ocr snippets. if more than one, separate with <br/>
47
+ #
48
+ # @param options [Hash] options hash provided by Blacklight
49
+ # @return [String] snippets HTML to be rendered
50
+ # rubocop:disable Rails/OutputSafety
51
+ def render_ocr_snippets(options = {})
52
+ snippets = options[:value]
53
+ snippets_content = [content_tag('div',
54
+ "... #{snippets.first} ...".html_safe,
55
+ class: 'ocr_snippet first_snippet')]
56
+ if snippets.length > 1
57
+ snippets_content << render(partial: 'catalog/snippets_more',
58
+ locals: { snippets: snippets.drop(1),
59
+ options: options })
60
+ end
61
+ snippets_content.join("\n").html_safe
62
+ end
63
+ # rubocop:enable Rails/OutputSafety
64
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IiifPrint
4
+ module ChildIndexer
5
+ ##
6
+ # @api private
7
+ #
8
+ # The goal of this method is to encapsulate the logic for what all we need for child
9
+ # relationships.
10
+ def self.decorate_work_types!
11
+ # TODO: This method is in the wrong location; says indexing but there's also the SetChildFlag
12
+ # consideration. Consider refactoring this stuff into a single nested module.
13
+ #
14
+
15
+ Hyrax.config.curation_concerns.each do |work_type|
16
+ work_type.send(:include, IiifPrint::SetChildFlag) unless work_type.included_modules.include?(IiifPrint::SetChildFlag)
17
+ indexer = work_type.indexer
18
+ unless indexer.respond_to?(:iiif_print_lineage_service)
19
+ indexer.prepend(self)
20
+ indexer.class_attribute(:iiif_print_lineage_service, default: IiifPrint::LineageService)
21
+ end
22
+ work_type::GeneratedResourceSchema.send(:include, IiifPrint::SetChildFlag)
23
+ end
24
+ end
25
+
26
+ def generate_solr_document
27
+ super.tap do |solr_doc|
28
+ solr_doc['is_child_bsi'] = object.is_child
29
+ solr_doc['is_page_of_ssim'] = iiif_print_lineage_service.ancestor_ids_for(object)
30
+ solr_doc['file_set_ids_ssim'] = iiif_print_lineage_service.descendent_file_set_ids_for(object)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IiifPrint
4
+ module FileSetIndexer
5
+ # Why `.decorate`? In my tests for Rails 5.2, I'm not able to use the prepended nor included
6
+ # blocks to assign a class_attribute when I "prepend" a module to the base class. This method
7
+ # allows me to handle that behavior.
8
+ #
9
+ # @param base [Class]
10
+ # @return [Class] the given base, now decorated in all of it's glory
11
+ def self.decorate(base)
12
+ base.prepend(self)
13
+ base.class_attribute :iiif_print_lineage_service, default: IiifPrint::LineageService
14
+ base
15
+ end
16
+
17
+ def generate_solr_document
18
+ super.tap do |solr_doc|
19
+ # only UV viewable images should have is_page_of, it is only used for iiif search
20
+ solr_doc['is_page_of_ssim'] = iiif_print_lineage_service.ancestor_ids_for(object) if object.mime_type&.match(/image/)
21
+ # index for full text search
22
+ text = IiifPrint::Data::WorkDerivatives.data(from: object, of_type: 'txt')
23
+ text = text.tr("\n", ' ').squeeze(' ')
24
+ solr_doc['all_text_timv'] = text
25
+ solr_doc['all_text_tsimv'] = text
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,8 @@
1
+ # Application Mailer
2
+ module IiifPrint
3
+ # Application Mailer Class
4
+ class ApplicationMailer < ActionMailer::Base
5
+ default from: 'from@example.com'
6
+ layout 'mailer'
7
+ end
8
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RDF
4
+ class CustomIsChildTerm < Vocabulary('http://id.loc.gov/vocabulary/identifiers/')
5
+ property 'is_child'
6
+ end
7
+ end
8
+
9
+ module IiifPrint
10
+ module SetChildFlag
11
+ extend ActiveSupport::Concern
12
+ included do
13
+ # Why the try? A work type's GeneratedResourceSchema goes through this path as well
14
+ # and does not have an #after_save resulting in a NoMethodError.
15
+ try(:after_save, :set_children)
16
+ property :is_child,
17
+ predicate: ::RDF::CustomIsChildTerm.is_child,
18
+ multiple: false do |index|
19
+ index.as :stored_searchable
20
+ end
21
+ end
22
+
23
+ def set_children
24
+ ordered_works.each do |child_work|
25
+ child_work.update(is_child: true) unless child_work.is_child
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,47 @@
1
+ module IiifPrint::Solr::Document
2
+ # @note Why decorate? We want to avoid including this module via generator. And the generator
3
+ # previously did two things: 1) include `IiifPrint::Solr::Document` in `SolrDocument`; 2)
4
+ # add the `attribute :is_child` field to the SolrDocument. We can't rely on `included do`
5
+ # block to handle that.
6
+ #
7
+ # This method is responsible for configuring the SolrDocument for a Hyrax/Hyku application. It
8
+ # does three things:
9
+ #
10
+ # 1. Adds instance methods to the SolrDocument (see implementation below)
11
+ # 2. Adds the `is_child` attribute to the SolrDocument
12
+ # 3. Adds a class attribute (e.g. `iiif_print_solr_field_names`) to allow further customization.
13
+ #
14
+ # @note These `iiif_print_solr_field_names` came from the newspaper_works implementation and are
15
+ # carried forward without much consideration, except to say "Make it configurable!"
16
+ #
17
+ # @param base [Class<SolrDocument>]
18
+ # @return [Class<SolrDocument>]
19
+ def self.decorate(base)
20
+ base.prepend(self)
21
+ base.send(:attribute, :is_child, Hyrax::SolrDocument::Metadata::Solr::String, 'is_child_bsi')
22
+
23
+ # @note These properties came from the newspaper_works gem. They are configurable.
24
+ base.class_attribute :iiif_print_solr_field_names, default: %w[alternative_title genre
25
+ issn lccn oclcnum held_by text_direction
26
+ page_number section author photographer
27
+ volume issue_number geographic_coverage
28
+ extent publication_date height width
29
+ edition_number edition_name frequency preceded_by
30
+ succeeded_by]
31
+ base
32
+ end
33
+
34
+ def method_missing(method_name, *args, &block)
35
+ super unless iiif_print_solr_field_names.include? method_name.to_s
36
+ self[::ActiveFedora.index_field_mapper.solr_name(method_name.to_s)]
37
+ end
38
+
39
+ def respond_to_missing?(method_name, include_private = false)
40
+ iiif_print_solr_field_names.include?(method_name.to_s) || super
41
+ end
42
+
43
+ # TODO: consider configuring this field name; we use the magic field in lots of places.
44
+ def file_set_ids
45
+ self['file_set_ids_ssim']
46
+ end
47
+ end
@@ -0,0 +1,6 @@
1
+ module IiifPrint
2
+ # Application Record Class
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ module IiifPrint
2
+ class DerivativeAttachment < ApplicationRecord
3
+ # We can store nil/optional fileset as interim value before fileset
4
+ # construction, but we require at minimum, path, destination_name
5
+ validates :path, presence: true
6
+ validates :destination_name, presence: true
7
+ end
8
+ end
@@ -0,0 +1,17 @@
1
+ module IiifPrint
2
+ module IiifSearchResponseDecorator
3
+ # Enable the user to search for child metadata in the parent's UV
4
+ # @see https://github.com/scientist-softserv/louisville-hyku/commit/67467e5cf9fdb755f54419f17d3c24c87032d0af
5
+ def annotation_list
6
+ json_results = super
7
+ json_results&.[]('resources')&.each do |result_hit|
8
+ next if result_hit['resource'].present?
9
+ result_hit['resource'] = {
10
+ "@type": "cnt:ContentAsText",
11
+ "chars": "Metadata match, see sidebar for details"
12
+ }
13
+ end
14
+ json_results
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ module IiifPrint
2
+ class IngestFileRelation < ApplicationRecord
3
+ validates :file_path, presence: true
4
+ validates :derivative_path, presence: true
5
+
6
+ # Query by file path for all derivatives, as de-duplicated array of
7
+ # derivative paths.
8
+ # @param path [String] Path to primary file
9
+ # @return [Array<String>] de-duplicated array of derivative paths.
10
+ def self.derivatives_for_file(path)
11
+ where(file_path: path).pluck(:derivative_path).uniq
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ module IiifPrint
2
+ class PendingRelationship < ApplicationRecord
3
+ validates :parent_id, presence: true
4
+ validates :child_title, presence: true
5
+ validates :child_order, presence: true
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ # mixin to provide URL for IIIF Content Search service
2
+ module IiifPrint
3
+ module IiifManifestPresenterBehavior
4
+ extend ActiveSupport::Concern
5
+
6
+ def search_service
7
+ Rails.application.routes.url_helpers.solr_document_iiif_search_url(id, host: hostname)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,33 @@
1
+ module IiifPrint
2
+ module IiifManifestPresenterFactoryBehavior
3
+ # This will override Hyrax::IiifManifestPresenter::Factory#build and introducing
4
+ # the expected behavior:
5
+ # - child work images show as canvases in the parent work manifest
6
+ # - child work images show in the uv on the parent show page
7
+ # - still create the manifest if the parent work has images attached but the child works do not
8
+ def build
9
+ ids.map do |id|
10
+ solr_doc = load_docs.find { |doc| doc.id == id }
11
+ next unless solr_doc
12
+
13
+ if solr_doc.file_set?
14
+ presenter_class.for(solr_doc)
15
+ elsif Hyrax.config.curation_concerns.include?(solr_doc.hydra_model)
16
+ # look up file set ids and loop through those
17
+ file_set_docs = load_file_set_docs(solr_doc.file_set_ids)
18
+ file_set_docs.map { |doc| presenter_class.for(doc) } if file_set_docs.length
19
+ end
20
+ end.flatten.compact
21
+ end
22
+
23
+ private
24
+
25
+ # still create the manifest if the parent work has images attached but the child works do not
26
+ def load_file_set_docs(file_set_ids)
27
+ return [] if file_set_ids.nil?
28
+
29
+ query("{!terms f=id}#{file_set_ids.join(',')}", rows: 1000)
30
+ .map { |res| ::SolrDocument.new(res) }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IiifPrint
4
+ module WorkShowPresenterDecorator
5
+ delegate :file_set_ids, to: :solr_document
6
+
7
+ # OVERRIDE Hyrax 2.9.6 to remove check for representative_presenter.image? and allow
8
+ # a fallback to check for images on the child works
9
+ # @return [Boolean] render a IIIF viewer
10
+ def iiif_viewer?
11
+ parent_work_has_files? || child_work_has_files?
12
+ end
13
+
14
+ alias universal_viewer? iiif_viewer?
15
+
16
+ private
17
+
18
+ def parent_work_has_files?
19
+ Hyrax.config.iiif_image_server? &&
20
+ representative_id.present? &&
21
+ representative_presenter.present? &&
22
+ members_include_viewable_image?
23
+ end
24
+
25
+ def child_work_has_files?
26
+ file_set_ids.present?
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # OVERRIDE Hyrax 2.9.6 to give user ability to display child works form linked facets
4
+ # We did work to display only parent works by default, for this client
5
+ module Hyrax
6
+ module Renderers
7
+ module FacetedAttributeRendererDecorator
8
+ private
9
+
10
+ # OVERRIDE Hyrax 2.9.6 to give user ability to display child works form linked facets
11
+ def search_path(value)
12
+ path = super(value)
13
+ path += '&include_child_works=true' if options[:is_child_bsi] == true
14
+ path
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ module IiifPrint
2
+ # hide Title, Container, and Issue objects if this is a keyword search
3
+ # can be added to default_processor_chain in a SearchBuilder class
4
+ module ExcludeModels
5
+ extend ActiveSupport::Concern
6
+
7
+ def exclude_models(solr_parameters, config: IiifPrint.config)
8
+ return unless solr_parameters[:q] || solr_parameters[:all_fields]
9
+
10
+ solr_parameters[:fq] ||= []
11
+ key = config.excluded_model_name_solr_field_key
12
+ config.excluded_model_name_solr_field_values.each do |value|
13
+ solr_parameters[:fq] << "-#{key}:\"#{value}\""
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ module IiifPrint
2
+ # add highlighting on _stored_ full text field if this is a keyword search
3
+ # can be added to default_processor_chain in a SearchBuilder class
4
+ module HighlightSearchParams
5
+ # add highlights on full text field, if there is a keyword query
6
+ def highlight_search_params(solr_parameters = {})
7
+ return unless solr_parameters[:q] || solr_parameters[:all_fields]
8
+ solr_parameters[:hl] = true
9
+ solr_parameters[:'hl.fl'] = 'all_text_tsimv'
10
+ solr_parameters[:'hl.fragsize'] = 100
11
+ solr_parameters[:'hl.snippets'] = 5
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,97 @@
1
+ module IiifPrint
2
+ module ManifestBuilderServiceBehavior
3
+ def initialize(*args,
4
+ version: IiifPrint.config.default_iiif_manifest_version,
5
+ iiif_manifest_factory: iiif_manifest_factory_for(version),
6
+ &block)
7
+ super(*args, iiif_manifest_factory: iiif_manifest_factory, &block)
8
+ @version = version.to_i
9
+ end
10
+
11
+ def manifest_for(presenter:)
12
+ build_manifest(presenter: presenter)
13
+ end
14
+
15
+ private
16
+
17
+ VERSION_TO_MANIFEST_FACTORY_MAP = {
18
+ 2 => ::IIIFManifest::ManifestFactory,
19
+ 3 => ::IIIFManifest::V3::ManifestFactory
20
+ }.freeze
21
+
22
+ def iiif_manifest_factory_for(version)
23
+ VERSION_TO_MANIFEST_FACTORY_MAP.fetch(version.to_i)
24
+ end
25
+
26
+ ##
27
+ # Allows for the display of metadata for child works in UV
28
+ #
29
+ # @see https://github.com/samvera/hyrax/blob/main/app/services/hyrax/manifest_builder_service.rb
30
+ def build_manifest(presenter:)
31
+ # ::IIIFManifest::ManifestBuilder#to_h returns a
32
+ # IIIFManifest::ManifestBuilder::IIIFManifest, not a Hash.
33
+ # to get a Hash, we have to call its #to_json, then parse.
34
+ #
35
+ # wild times. maybe there's a better way to do this with the
36
+ # ManifestFactory interface?
37
+ manifest = manifest_factory.new(presenter).to_h
38
+ hash = JSON.parse(manifest.to_json)
39
+ hash = send("sanitize_v#{@version}", hash: hash, presenter: presenter)
40
+ send("sorted_canvases_v#{@version}", hash: hash, sort_field: IiifPrint.config.sort_iiif_manifest_canvases_by)
41
+ end
42
+
43
+ def sanitize_v2(hash:, presenter:)
44
+ hash['label'] = CGI.unescapeHTML(sanitize_value(hash['label'])) if hash.key?('label')
45
+ hash.delete('description') # removes default description since it's in the metadata fields
46
+ hash['sequences']&.each do |sequence|
47
+ sequence['canvases']&.each do |canvas|
48
+ canvas['label'] = CGI.unescapeHTML(sanitize_value(canvas['label']))
49
+ apply_v2_metadata_to_canvas(canvas: canvas, presenter: presenter)
50
+ end
51
+ end
52
+ hash
53
+ end
54
+
55
+ def sanitize_v3(hash:, **)
56
+ # TODO: flesh out metadata for v3
57
+ hash
58
+ end
59
+
60
+ def apply_v2_metadata_to_canvas(canvas:, presenter:)
61
+ solr_docs = get_solr_docs(presenter)
62
+ # uses the '@id' property which is a URL that contains the FileSet id
63
+ file_set_id = canvas['@id'].split('/').last
64
+ # finds the image that the FileSet is attached to and creates metadata on that canvas
65
+ image = solr_docs.find { |doc| doc[:member_ids_ssim]&.include?(file_set_id) }
66
+ canvas_metadata = IiifPrint.manifest_metadata_for(work: image,
67
+ current_ability: presenter.ability,
68
+ base_url: presenter.base_url)
69
+ canvas['metadata'] = canvas_metadata
70
+ end
71
+
72
+ def sorted_canvases_v2(hash:, sort_field:)
73
+ sort_field = Hyrax::Renderers::AttributeRenderer.new(sort_field, nil).label
74
+ hash["sequences"]&.first&.[]("canvases")&.sort_by! do |canvas|
75
+ selection = canvas["metadata"].select { |h| h["label"] == sort_field }
76
+ fallback = [{ label: sort_field, value: ['~'] }]
77
+ identifier_metadata = selection.presence || fallback
78
+ identifier_metadata.first["value"] if identifier_metadata.present?
79
+ end
80
+ hash
81
+ end
82
+
83
+ def sorted_canvases_v3(hash:, **)
84
+ # TODO: flesh out metadata for v3
85
+ hash
86
+ end
87
+
88
+ def get_solr_docs(presenter)
89
+ parent_id = [presenter._source['id']]
90
+ child_ids = presenter._source['member_ids_ssim']
91
+ parent_id_and_child_ids = parent_id + child_ids
92
+ query = ActiveFedora::SolrQueryBuilder.construct_query_for_ids(parent_id_and_child_ids)
93
+ solr_hits = ActiveFedora::SolrService.query(query, fq: "-has_model_ssim:FileSet", rows: 100_000)
94
+ solr_hits.map { |solr_hit| ::SolrDocument.new(solr_hit) }
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,120 @@
1
+ # General derivative service for IiifPrint, which is meant to wrap
2
+ # and replace the stock Hyrax::FileSetDerivativeService with a proxy
3
+ # that runs one or more derivative service "plugin" components.
4
+ #
5
+ # Note: Hyrax::DerivativeService consumes this, instead of (directly)
6
+ # consuming Hyrax::FileSetDerivativeService.
7
+ #
8
+ # Unlike the "run the first valid plugin" arrangement that the
9
+ # Hyrax::DerivativeService uses to run an actual derivative creation
10
+ # service component, this component is:
11
+ #
12
+ # (a) Consumed by Hyrax::DerivativeService as that first valid plugin;
13
+ #
14
+ # (b) Wraps and runs 0..* plugins, not just the first.
15
+ #
16
+ # This should be registered to take precedence over default by:
17
+ # Hyrax::DerivativeService.services.unshift(
18
+ # IiifPrint::PluggableDerivativeService
19
+ # )
20
+ #
21
+ # Modify IiifPrint::PluggableDerivativeService.plugins
22
+ # to add, remove, or reorder plugin (derivative service) classes.
23
+ #
24
+ class IiifPrint::PluggableDerivativeService
25
+ class_attribute :allowed_methods, default: [:cleanup_derivatives, :create_derivatives]
26
+ class_attribute :default_plugins, default: [Hyrax::FileSetDerivativesService]
27
+ class_attribute :derivative_path_factory, default: Hyrax::DerivativePath
28
+
29
+ def initialize(file_set, plugins: plugins_for(file_set))
30
+ @file_set = file_set
31
+ @plugins = Array.wrap(plugins)
32
+ @valid_plugins = plugins.map { |plugin| plugin.new(file_set) }.select(&:valid?)
33
+ end
34
+
35
+ attr_reader :file_set, :plugins, :valid_plugins
36
+ delegate :uri, :mime_type, to: :file_set
37
+
38
+ # this wrapper/proxy/composite is always valid, but it may compose
39
+ # multiple plugins, some of which may or may not be valid, so
40
+ # validity checks happen within as well.
41
+ def valid?
42
+ !valid_plugins.size.zero?
43
+ end
44
+
45
+ # get derivative services relevant to method name and file_set context
46
+ # -- omits plugins if particular destination exists or will soon.
47
+ def services(method_name)
48
+ valid_plugins.select do |plugin|
49
+ dest = nil
50
+ dest = plugin.target_extension if plugin.respond_to?(:target_extension)
51
+ !skip_destination?(method_name, dest)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def respond_to_missing?(method_name, include_private = false)
58
+ allowed_methods.include?(method_name) || super
59
+ end
60
+
61
+ def method_missing(method_name, *args, **opts, &block)
62
+ if allowed_methods.include?(method_name)
63
+ # we have an allowed method, construct services and include all valid
64
+ # services for the file_set
65
+ # services = plugins.map { |plugin| plugin.new(file_set) }.select(&:valid?)
66
+ # run all valid services, in order:
67
+ services(method_name).each do |plugin|
68
+ plugin.send(method_name, *args, **opts, &block)
69
+ end
70
+ else
71
+ super
72
+ end
73
+ end
74
+
75
+ def skip_destination?(method_name, destination_name)
76
+ return false unless method_name == :create_derivatives
77
+ return false unless destination_name
78
+ # NOTE: What are we after with this nil test? Are we looking for persisted objects?
79
+ return false if file_set.id.nil?
80
+
81
+ # skip :create_derivatives if existing --> do not re-create
82
+ existing_derivative?(destination_name) ||
83
+ impending_derivative?(destination_name)
84
+ end
85
+
86
+ def existing_derivative?(name)
87
+ path = derivative_path_factory.derivative_path_for_reference(
88
+ file_set,
89
+ name
90
+ )
91
+ File.exist?(path)
92
+ end
93
+
94
+ # is there an impending attachment from ingest logged to db?
95
+ # -- avoids stomping over pre-made derivative
96
+ # for which an attachment is still in-progress.
97
+ def impending_derivative?(name)
98
+ IiifPrint::DerivativeAttachment.exists?(
99
+ fileset_id: file_set.id,
100
+ destination_name: name
101
+ )
102
+ end
103
+
104
+ # This method is responsible for determine what are the possible plugins / services that this file
105
+ # set would use. That "possibility" is based on the work. Later, we will check the plugin's
106
+ # "valid?" which would now look at the specific file_set for validity.
107
+ def plugins_for(file_set)
108
+ parent = parent_for(file_set)
109
+ return Array(default_plugins) if parent.nil?
110
+ return Array(default_plugins) unless parent.respond_to?(:iiif_print_config)
111
+
112
+ (file_set.parent.iiif_print_config.derivative_service_plugins + Array(default_plugins)).flatten.compact.uniq
113
+ end
114
+
115
+ def parent_for(file_set)
116
+ # fallback to Fedora-stored relationships if work's aggregation of
117
+ # file set is not indexed in Solr
118
+ file_set.parent || file_set.member_of.find(&:work?)
119
+ end
120
+ end
@@ -0,0 +1,16 @@
1
+ <%# additional ocr snippets, with a Bootstrap collapse toggle control %>
2
+ <% document_id = options[:document].id %>
3
+ <div class="collapse ocr_snippet" id="<%= "snippet_collapse_#{document_id}" %>">
4
+ <% snippets.each do |snippet| %>
5
+ <%= content_tag('div',
6
+ "... #{snippet} ...".html_safe,
7
+ class: 'ocr_snippet') %>
8
+ <% end %>
9
+ </div>
10
+ <%= link_to(t('blacklight.search.results.snippets.more'),
11
+ "#snippet_collapse_#{document_id}",
12
+ data: {toggle: 'collapse'},
13
+ 'aria-expanded' => 'false',
14
+ 'aria-controls' => "#snippet_collapse_#{document_id}",
15
+ class: 'ocr_snippets_expand js-controls')
16
+ %>
@@ -0,0 +1,9 @@
1
+ <% if presenter.iiif_viewer? %>
2
+ <% if defined?(viewer) && viewer %>
3
+ <%= iiif_viewer_display presenter %>
4
+ <% else %>
5
+ <%= render media_display_partial(presenter.representative_presenter), file_set: presenter.representative_presenter %>
6
+ <% end %>
7
+ <% else %>
8
+ <%= image_tag 'default.png', class: "canonical-image", alt: 'default representative image' %>
9
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <div class="viewer-wrapper">
2
+ <iframe
3
+ id="uv-iframe"
4
+ src="<%= universal_viewer_base_url %>#?manifest=<%= main_app.polymorphic_url [main_app, :manifest, presenter], { locale: nil } %>&config=<%= universal_viewer_config_url %>"
5
+ allowfullscreen="true"
6
+ frameborder="0"
7
+ ></iframe>
8
+ </div>