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