Skip to content

Commit

Permalink
IIIF #5 - Updating resource to extract EXIF data after save
Browse files Browse the repository at this point in the history
  • Loading branch information
dleadbetter committed Jul 18, 2022
1 parent 25296ab commit 6bf18e1
Show file tree
Hide file tree
Showing 11 changed files with 214 additions and 28 deletions.
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ gem 'aws-sdk-s3'
# Image processing
gem 'mini_magick', '~> 4.11'

# EXIF data
gem 'exif', '~> 2.2.3'

group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem 'debug', platforms: %i[ mri mingw x64_mingw ]
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ GEM
dotenv (= 2.7.6)
railties (>= 3.2)
erubi (1.10.0)
exif (2.2.3)
globalid (1.0.0)
activesupport (>= 5.0)
i18n (1.10.0)
Expand Down Expand Up @@ -218,6 +219,7 @@ DEPENDENCIES
controlled_vocabulary!
debug
dotenv-rails
exif (~> 2.2.3)
jwt
mini_magick (~> 4.11)
pagy (~> 5.10)
Expand Down
2 changes: 1 addition & 1 deletion app/models/concerns/attachable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def generate_url_method(name)
attachment = self.send(name)
return nil unless attachment.attached?

"#{self.send("#{name}_base_url")}/full/500,/0/default.jpg"
"#{self.send("#{name}_base_url")}/full/^500,/0/default.jpg"
end

define_method("#{name}_thumbnail_url") do
Expand Down
15 changes: 13 additions & 2 deletions app/models/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def content_metadata
def content_preview_url
return attachable_content_preview_url unless content.image? && content_converted.attached?

"#{content_base_url}/full/500,/0/default.jpg"
"#{content_base_url}/full/^500,/0/default.jpg"
end

def content_thumbnail_url
Expand All @@ -66,6 +66,7 @@ def content_type
def after_create
convert
create_manifest
extract_exif
end

private
Expand All @@ -86,10 +87,20 @@ def convert
end

def create_manifest
self.manifest = Images::Manifest.create(self)
self.manifest = Iiif::Manifest.create(self)
save
end


def extract_exif
return unless content.attached? && content.image?

content.open do |file|
self.exif = JSON.dump(Images::Exif.extract(file))
save
end
end

def set_uuid
self.uuid = SecureRandom.uuid
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module Images
module Iiif
class Manifest
def self.create(resource)
manifest = to_json('manifest.json')
Expand Down
28 changes: 28 additions & 0 deletions app/services/images/exif.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module Images
class Exif
def self.extract(file)
begin
data = ::Exif::Data.new(File.open(file.path)).to_h
encode data
rescue
# Do nothing. The image may not contain EXIF data
end
end

private

def self.encode(hash)
hash.keys.each do |key|
value = hash[key]

if value.is_a?(String)
hash[key] = hash[key].force_encoding('ISO-8859-1').encode('UTF-8')
elsif value.is_a?(Hash)
hash[key] = encode(hash[key])
end
end

hash
end
end
end
102 changes: 102 additions & 0 deletions client/src/components/ResourceExifModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// @flow

import React, { useCallback, type ComponentType } from 'react';
import {
Button,
Grid,
Header,
Modal,
Table
} from 'semantic-ui-react';
import _ from 'underscore';
import { useTranslation } from 'react-i18next';

type Props = {
exif: any,
onClose: () => void
};

const ResourceExifModal: ComponentType<any> = (props: Props) => {
const { t } = useTranslation();

/**
* Renders the header component and table structure.
*
* @type {function([*,*])}
*/
const renderHeader = useCallback(([key, value]) => (
<>
<Header
content={key}
/>
<Table
columns={2}
padded
striped
>
<Table.Body>
{ _.map(Object.entries(value), renderItem) }
{ _.isEmpty(value) && (
<Table.Row>
<Table.Cell>
No records
</Table.Cell>
</Table.Row>
)}
</Table.Body>
</Table>
</>
), []);

/**
* Renders the table row for the passed key/value.
*
* @type {function([*,*])}
*/
const renderItem = useCallback(([key, value]) => (
<Table.Row>
<Table.Cell>{ key }</Table.Cell>
<Table.Cell>{ value }</Table.Cell>
</Table.Row>
), []);

return (
<Modal
centered={false}
open
>
<Modal.Header>
<Grid
columns={2}
>
<Grid.Column>
<Header
content={t('ResourceExifModal.title')}
/>
</Grid.Column>
<Grid.Column
textAlign='right'
>
<Button
basic
content={t('Common.buttons.close')}
onClick={props.onClose}
/>
</Grid.Column>
</Grid>
</Modal.Header>
<Modal.Content>
{ _.map(Object.entries(props.exif), renderHeader) }
</Modal.Content>
<Modal.Actions>
<Button
basic
content={t('Common.buttons.close')}
onClick={props.onClose}
/>
</Modal.Actions>
</Modal>
);
};

export default ResourceExifModal;
30 changes: 30 additions & 0 deletions client/src/components/ResourceViewerModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// @flow

import CloverIIIF from '@samvera/clover-iiif';
import React, { type ComponentType } from 'react';
import { Modal } from 'semantic-ui-react';

type Props = {
manifestId: string,
onClose: () => void
};

const ResourceViewerModal: ComponentType<any> = (props: Props) => (
<Modal
centered={false}
closeIcon
onClose={props.onClose}
open
>
<Modal.Content>
<CloverIIIF
manifestId={props.manifestId}
options={{
showIIIFBadge: false
}}
/>
</Modal.Content>
</Modal>
);

export default ResourceViewerModal;
7 changes: 7 additions & 0 deletions client/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"buttons": {
"add": "Add",
"cancel": "Cancel",
"close": "Close",
"download": "Download",
"iiif": "View IIIF",
"remove": "Remove",
Expand Down Expand Up @@ -122,13 +123,19 @@
}
},
"Resource": {
"buttons": {
"exif": "View Info"
},
"errors": {
"required": "{{name}} is required"
},
"labels": {
"content": "Content"
}
},
"ResourceExifModal": {
"title": "Resource EXIF Information"
},
"Routes": {
"organizationDetails": "Organization Edit",
"organizations": "Organizations",
Expand Down
47 changes: 25 additions & 22 deletions client/src/pages/Resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { FileInputButton, LazyImage } from '@performant-software/semantic-components';
import { Object as ObjectUtils } from '@performant-software/shared-components';
import CloverIIIF from '@samvera/clover-iiif';
import React, {
useEffect,
useMemo,
Expand All @@ -11,21 +10,19 @@ import React, {
} from 'react';
import { withTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import {
Button,
Form,
Icon,
Modal
} from 'semantic-ui-react';
import { Button, Form, Icon } from 'semantic-ui-react';
import _ from 'underscore';
import i18n from '../i18n/i18n';
import ProjectsService from '../services/Projects';
import ResourceExifModal from '../components/ResourceExifModal';
import ResourceMetadata from '../components/ResourceMetadata';
import ResourceViewerModal from '../components/ResourceViewerModal';
import ResourcesService from '../services/Resources';
import SimpleEditPage from '../components/SimpleEditPage';
import withEditPage from '../hooks/EditPage';

const ResourceForm = withTranslation()((props) => {
const [info, setInfo] = useState(false);
const [project, setProject] = useState();
const [viewer, setViewer] = useState(false);

Expand Down Expand Up @@ -101,6 +98,17 @@ const ResourceForm = withTranslation()((props) => {
onClick={() => setViewer(true)}
/>
)}
{ props.item.exif && (
<Button
content={props.t('Resource.buttons.exif')}
icon='info circle'
onClick={() => setInfo(true)}
style={{
backgroundColor: '#219ebc',
color: '#FFFFFF'
}}
/>
)}
{ props.item.content_download_url && (
<a
className='ui button green'
Expand Down Expand Up @@ -138,22 +146,17 @@ const ResourceForm = withTranslation()((props) => {
value={props.item.metadata && JSON.parse(props.item.metadata)}
/>
)}
{ viewer && props.item.manifest && (
<Modal
centered={false}
closeIcon
{ viewer && manifestId && (
<ResourceViewerModal
manifestId={manifestId}
onClose={() => setViewer(false)}
open={viewer}
>
<Modal.Content>
<CloverIIIF
manifestId={manifestId}
options={{
showIIIFBadge: false
}}
/>
</Modal.Content>
</Modal>
/>
)}
{ info && props.item.exif && (
<ResourceExifModal
exif={JSON.parse(props.item.exif)}
onClose={() => setInfo(false)}
/>
)}
</SimpleEditPage.Tab>
</SimpleEditPage>
Expand Down
4 changes: 2 additions & 2 deletions db/migrate/20220706163617_create_resources.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ def up
create_table :resources do |t|
t.references :project, null: false, foreign_key: true, index: true
t.string :name
t.jsonb :exif
t.jsonb :metadata
t.text :exif
t.text :metadata

t.timestamps
end
Expand Down

0 comments on commit 6bf18e1

Please sign in to comment.