Switch to exifr for image metadata extraction (#6922)

* Switch to exifr for image metadata extraction

* Fix migrations on pg

* Prevent double divider

Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
Pascal Jufer
2021-08-06 01:19:18 +02:00
committed by GitHub
parent f9bf7853c9
commit bea3794f0a
9 changed files with 204 additions and 158 deletions

View File

@@ -98,14 +98,13 @@
"dotenv": "^10.0.0",
"eventemitter2": "^6.4.3",
"execa": "^5.1.1",
"exif-reader": "^1.0.3",
"exifr": "^7.1.2",
"express": "^4.17.1",
"express-session": "^1.17.2",
"fs-extra": "^10.0.0",
"grant": "^5.4.14",
"graphql": "^15.5.0",
"graphql-compose": "^9.0.1",
"icc": "^2.0.0",
"inquirer": "^8.1.1",
"joi": "^17.3.0",
"js-yaml": "^4.1.0",

View File

@@ -0,0 +1,94 @@
import { Knex } from 'knex';
// Change image metadata structure to match the output from 'exifr'
export async function up(knex: Knex): Promise<void> {
const files = await knex
.select<{ id: number; metadata: string }[]>('id', 'metadata')
.from('directus_files')
.whereNotNull('metadata');
for (const { id, metadata } of files) {
let prevMetadata;
try {
prevMetadata = JSON.parse(metadata);
} catch {
continue;
}
// Update only required if metadata has 'exif' data
if (prevMetadata.exif) {
// Get all data from 'exif' and rename the following keys:
// - 'image' to 'ifd0'
// - 'thumbnail to 'ifd1'
// - 'interoperability' to 'interop'
const newMetadata = prevMetadata.exif;
if (newMetadata.image) {
newMetadata.ifd0 = newMetadata.image;
delete newMetadata.image;
}
if (newMetadata.thumbnail) {
newMetadata.ifd1 = newMetadata.thumbnail;
delete newMetadata.thumbnail;
}
if (newMetadata.interoperability) {
newMetadata.interop = newMetadata.interoperability;
delete newMetadata.interoperability;
}
if (prevMetadata.icc) {
newMetadata.icc = prevMetadata.icc;
}
if (prevMetadata.iptc) {
newMetadata.iptc = prevMetadata.iptc;
}
await knex('directus_files')
.update({ metadata: JSON.stringify(newMetadata) })
.where({ id });
}
}
}
export async function down(knex: Knex): Promise<void> {
const files = await knex
.select<{ id: number; metadata: string }[]>('id', 'metadata')
.from('directus_files')
.whereNotNull('metadata')
.whereNot('metadata', '{}');
for (const { id, metadata } of files) {
const prevMetadata = JSON.parse(metadata);
// Update only required if metadata has keys other than 'icc' and 'iptc'
if (Object.keys(prevMetadata).filter((key) => key !== 'icc' && key !== 'iptc').length > 0) {
// Put all data under 'exif' and rename/move keys afterwards
const newMetadata: { exif: Record<string, unknown>; icc?: unknown; iptc?: unknown } = { exif: prevMetadata };
if (newMetadata.exif.ifd0) {
newMetadata.exif.image = newMetadata.exif.ifd0;
delete newMetadata.exif.ifd0;
}
if (newMetadata.exif.ifd1) {
newMetadata.exif.thumbnail = newMetadata.exif.ifd1;
delete newMetadata.exif.ifd1;
}
if (newMetadata.exif.interop) {
newMetadata.exif.interoperability = newMetadata.exif.interop;
delete newMetadata.exif.interop;
}
if (newMetadata.exif.icc) {
newMetadata.icc = newMetadata.exif.icc;
delete newMetadata.exif.icc;
}
if (newMetadata.exif.iptc) {
newMetadata.iptc = newMetadata.exif.iptc;
delete newMetadata.exif.iptc;
}
await knex('directus_files')
.update({ metadata: JSON.stringify(newMetadata) })
.where({ id });
}
}
}

View File

@@ -1,7 +1,6 @@
import formatTitle from '@directus/format-title';
import axios, { AxiosResponse } from 'axios';
import parseEXIF from 'exif-reader';
import { parse as parseICC } from 'icc';
import exifr from 'exifr';
import { clone } from 'lodash';
import { extension } from 'mime-types';
import path from 'path';
@@ -13,7 +12,6 @@ import { ForbiddenException, ServiceUnavailableException } from '../exceptions';
import logger from '../logger';
import storage from '../storage';
import { AbstractServiceOptions, File, PrimaryKey } from '../types';
import parseIPTC from '../utils/parse-iptc';
import { toArray } from '@directus/shared/utils';
import { ItemsService, MutationOptions } from './items';
@@ -86,37 +84,30 @@ export class FilesService extends ItemsService {
payload.height = meta.height;
}
payload.filesize = meta.size;
payload.metadata = {};
if (meta.icc) {
try {
payload.metadata.icc = parseICC(meta.icc);
} catch (err) {
logger.warn(`Couldn't extract ICC information from file`);
logger.warn(err);
try {
payload.metadata = await exifr.parse(buffer.content, {
icc: true,
iptc: true,
ifd1: true,
interop: true,
translateValues: true,
reviveValues: true,
mergeOutput: false,
});
if (payload.metadata?.iptc?.Headline) {
payload.title = payload.metadata.iptc.Headline;
}
}
if (meta.exif) {
try {
payload.metadata.exif = parseEXIF(meta.exif);
} catch (err) {
logger.warn(`Couldn't extract EXIF information from file`);
logger.warn(err);
if (!payload.description && payload.metadata?.iptc?.Caption) {
payload.description = payload.metadata.iptc.Caption;
}
}
if (meta.iptc) {
try {
payload.metadata.iptc = parseIPTC(meta.iptc);
payload.title = payload.metadata.iptc.headline || payload.title;
payload.description = payload.description || payload.metadata.iptc.caption;
payload.tags = payload.metadata.iptc.keywords;
} catch (err) {
logger.warn(`Couldn't extract IPTC information from file`);
logger.warn(err);
if (payload.metadata?.iptc?.Keywords) {
payload.tags = payload.metadata.iptc.Keywords;
}
} catch (err) {
logger.warn(`Couldn't extract metadata from file`);
logger.warn(err);
}
}

View File

@@ -3,16 +3,6 @@ declare module 'grant' {
export default grant;
}
declare module 'icc' {
const parse: (buf: Buffer) => Record<string, string>;
export { parse };
}
declare module 'exif-reader' {
const exifReader: (buf: Buffer) => Record<string, any>;
export default exifReader;
}
declare module 'pino-http' {
import PinoHttp from '@types/pino-http';
const pinoHttp: PinoHttp;

View File

@@ -1,51 +0,0 @@
const IPTC_ENTRY_TYPES = new Map([
[0x78, 'caption'],
[0x6e, 'credit'],
[0x19, 'keywords'],
[0x37, 'dateCreated'],
[0x50, 'byline'],
[0x55, 'bylineTitle'],
[0x7a, 'captionWriter'],
[0x69, 'headline'],
[0x74, 'copyright'],
[0x0f, 'category'],
]);
const IPTC_ENTRY_MARKER = Buffer.from([0x1c, 0x02]);
export default function parseIPTC(buffer: Buffer): Record<string, any> {
if (!Buffer.isBuffer(buffer)) return {};
const iptc: Record<string, any> = {};
let lastIptcEntryPos = buffer.indexOf(IPTC_ENTRY_MARKER);
while (lastIptcEntryPos !== -1) {
lastIptcEntryPos = buffer.indexOf(IPTC_ENTRY_MARKER, lastIptcEntryPos + IPTC_ENTRY_MARKER.byteLength);
const iptcBlockTypePos = lastIptcEntryPos + IPTC_ENTRY_MARKER.byteLength;
const iptcBlockSizePos = iptcBlockTypePos + 1;
const iptcBlockDataPos = iptcBlockSizePos + 2;
const iptcBlockType = buffer.readUInt8(iptcBlockTypePos);
const iptcBlockSize = buffer.readUInt16BE(iptcBlockSizePos);
if (!IPTC_ENTRY_TYPES.has(iptcBlockType)) {
continue;
}
const iptcBlockTypeId = IPTC_ENTRY_TYPES.get(iptcBlockType);
const iptcData = buffer.slice(iptcBlockDataPos, iptcBlockDataPos + iptcBlockSize).toString();
if (iptcBlockTypeId) {
if (iptc[iptcBlockTypeId] == null) {
iptc[iptcBlockTypeId] = iptcData;
} else if (Array.isArray(iptc[iptcBlockTypeId])) {
iptc[iptcBlockTypeId].push(iptcData);
} else {
iptc[iptcBlockTypeId] = [iptc[iptcBlockTypeId], iptcData];
}
}
}
return iptc;
}

View File

@@ -408,7 +408,7 @@ documentation: Documentation
sidebar: Sidebar
duration: Duration
charset: Charset
second: Second
second: second
file_moved: File Moved
collection_created: Collection Created
modified_on: Modified On

View File

@@ -80,32 +80,41 @@
</dd>
</div>
<template v-if="file.metadata && file.metadata.exif && file.metadata.exif.exif && file.metadata.exif.image">
<template
v-if="
file.metadata.ifd0?.Make ||
file.metadata.ifd0?.Model ||
file.metadata.exif?.FNumber ||
file.metadata.exif?.ExposureTime ||
file.metadata.exif?.FocalLength ||
file.metadata.exif?.ISO
"
>
<v-divider />
<div v-if="file.metadata.exif.image.Make && file.metadata.exif.image.Model">
<div v-if="file.metadata.ifd0?.Make && file.metadata.ifd0?.Model">
<dt>{{ t('camera') }}</dt>
<dd>{{ file.metadata.exif.image.Make }} {{ file.metadata.exif.image.Model }}</dd>
<dd>{{ file.metadata.ifd0.Make }} {{ file.metadata.ifd0.Model }}</dd>
</div>
<div v-if="file.metadata.exif.exif.FNumber">
<div v-if="file.metadata.exif?.FNumber">
<dt>{{ t('exposure') }}</dt>
<dd>ƒ/{{ file.metadata.exif.exif.FNumber }}</dd>
<dd>ƒ/{{ file.metadata.exif.FNumber }}</dd>
</div>
<div v-if="file.metadata.exif.exif.ExposureTime">
<div v-if="file.metadata.exif?.ExposureTime">
<dt>{{ t('shutter') }}</dt>
<dd>1/{{ Math.round(1 / +file.metadata.exif.exif.ExposureTime) }} {{ t('second') }}</dd>
<dd>1/{{ Math.round(1 / +file.metadata.exif.ExposureTime) }} {{ t('second') }}</dd>
</div>
<div v-if="file.metadata.exif.exif.FocalLength">
<div v-if="file.metadata.exif?.FocalLength">
<dt>{{ t('focal_length') }}</dt>
<dd>{{ file.metadata.exif.exif.FocalLength }}mm</dd>
<dd>{{ file.metadata.exif.FocalLength }}mm</dd>
</div>
<div v-if="file.metadata.exif.exif.ISO">
<div v-if="file.metadata.exif?.ISO">
<dt>{{ t('iso') }}</dt>
<dd>{{ file.metadata.exif.exif.ISO }}</dd>
<dd>{{ file.metadata.exif.ISO }}</dd>
</div>
</template>
</dl>

View File

@@ -7,51 +7,80 @@ _Changes marked with a :warning: contain potential breaking changes depending on
### :sparkles: New Features
- **App**
- [#7130](https://github.com/directus/directus/pull/7130) Add accordion group ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7101](https://github.com/directus/directus/pull/7101) Surface dropdown choices in advanced sidebar filter ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7130](https://github.com/directus/directus/pull/7130) Add accordion group
([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7101](https://github.com/directus/directus/pull/7101) Surface dropdown choices in advanced sidebar filter
([@rijkvanzanten](https://github.com/rijkvanzanten))
### :rocket: Improvements
- **App**
- [#7141](https://github.com/directus/directus/pull/7141) Title format repeater names ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7132](https://github.com/directus/directus/pull/7132) Add missing keys to translations ([@nickrum](https://github.com/nickrum))
- [#7103](https://github.com/directus/directus/pull/7103) Add a standardized max-height to tree select interface ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7102](https://github.com/directus/directus/pull/7102) Render list group arrows on the left of the group checkbox in the tree select interface ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7059](https://github.com/directus/directus/pull/7059) Added "Default Open" Checkbox to Field Group Dividers ([@m0rtis0](https://github.com/m0rtis0))
- [#7141](https://github.com/directus/directus/pull/7141) Title format repeater names
([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7132](https://github.com/directus/directus/pull/7132) Add missing keys to translations
([@nickrum](https://github.com/nickrum))
- [#7103](https://github.com/directus/directus/pull/7103) Add a standardized max-height to tree select interface
([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7102](https://github.com/directus/directus/pull/7102) Render list group arrows on the left of the group checkbox
in the tree select interface ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7059](https://github.com/directus/directus/pull/7059) Added "Default Open" Checkbox to Field Group Dividers
([@m0rtis0](https://github.com/m0rtis0))
- **API**
- [#7105](https://github.com/directus/directus/pull/7105) Stall login/pw reset to prevent email leaking ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#6580](https://github.com/directus/directus/pull/6580) Warn on Missing Migrations ([@jaycammarano](https://github.com/jaycammarano))
- [#7105](https://github.com/directus/directus/pull/7105) Stall login/pw reset to prevent email leaking
([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#6580](https://github.com/directus/directus/pull/6580) Warn on Missing Migrations
([@jaycammarano](https://github.com/jaycammarano))
### :bug: Bug Fixes
- **App**
- [#7142](https://github.com/directus/directus/pull/7142) Prevent duplicate alias fields from being created ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7135](https://github.com/directus/directus/pull/7135) Fix nested fields check in validate-payload handler ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7131](https://github.com/directus/directus/pull/7131) Fix default value of select-icon interface ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7142](https://github.com/directus/directus/pull/7142) Prevent duplicate alias fields from being created
([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7135](https://github.com/directus/directus/pull/7135) Fix nested fields check in validate-payload handler
([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7131](https://github.com/directus/directus/pull/7131) Fix default value of select-icon interface
([@rijkvanzanten](https://github.com/rijkvanzanten))
- **API**
- [#7139](https://github.com/directus/directus/pull/7139) Fix cache-key generation for query params ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7104](https://github.com/directus/directus/pull/7104) Fix users accountability tracking ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7139](https://github.com/directus/directus/pull/7139) Fix cache-key generation for query params
([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7104](https://github.com/directus/directus/pull/7104) Fix users accountability tracking
([@rijkvanzanten](https://github.com/rijkvanzanten))
### :memo: Documentation
- [#7106](https://github.com/directus/directus/pull/7106) Add note on conditional fields ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7099](https://github.com/directus/directus/pull/7099) Add note regarding required directus:extension field to extension docs ([@nickrum](https://github.com/nickrum))
- [#7079](https://github.com/directus/directus/pull/7079) Add note on npm run dev restart ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7077](https://github.com/directus/directus/pull/7077) Add note on hook params ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7106](https://github.com/directus/directus/pull/7106) Add note on conditional fields
([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7099](https://github.com/directus/directus/pull/7099) Add note regarding required directus:extension field to
extension docs ([@nickrum](https://github.com/nickrum))
- [#7079](https://github.com/directus/directus/pull/7079) Add note on npm run dev restart
([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7077](https://github.com/directus/directus/pull/7077) Add note on hook params
([@rijkvanzanten](https://github.com/rijkvanzanten))
### :package: Dependency Updates
- [#7136](https://github.com/directus/directus/pull/7136) update typescript-eslint monorepo to v4.29.0 ([@renovate[bot]](https://github.com/apps/renovate))
- [#7117](https://github.com/directus/directus/pull/7117) update dependency joi to v17.4.2 ([@renovate[bot]](https://github.com/apps/renovate))
- [#7115](https://github.com/directus/directus/pull/7115) update dependency knex to v0.95.9 ([@renovate[bot]](https://github.com/apps/renovate))
- [#7110](https://github.com/directus/directus/pull/7110) update dependency sass to v1.37.0 ([@renovate[bot]](https://github.com/apps/renovate))
- [#7109](https://github.com/directus/directus/pull/7109) update dependency eslint to v7.32.0 ([@renovate[bot]](https://github.com/apps/renovate))
- [#7094](https://github.com/directus/directus/pull/7094) update dependency @rollup/plugin-commonjs to v20 ([@renovate[bot]](https://github.com/apps/renovate))
- [#7093](https://github.com/directus/directus/pull/7093) update dependency chalk to v4.1.2 ([@renovate[bot]](https://github.com/apps/renovate))
- [#7090](https://github.com/directus/directus/pull/7090) update dependency npm-watch to v0.11.0 ([@renovate[bot]](https://github.com/apps/renovate))
- [#7089](https://github.com/directus/directus/pull/7089) update dependency eslint-plugin-vue to v7.15.0 ([@renovate[bot]](https://github.com/apps/renovate))
- [#7087](https://github.com/directus/directus/pull/7087) update styfle/cancel-workflow-action action to v0.9.1 ([@renovate[bot]](https://github.com/apps/renovate))
- [#7085](https://github.com/directus/directus/pull/7085) update dependency rollup to v2.55.1 ([@renovate[bot]](https://github.com/apps/renovate))
- [#7136](https://github.com/directus/directus/pull/7136) update typescript-eslint monorepo to v4.29.0
([@renovate[bot]](https://github.com/apps/renovate))
- [#7117](https://github.com/directus/directus/pull/7117) update dependency joi to v17.4.2
([@renovate[bot]](https://github.com/apps/renovate))
- [#7115](https://github.com/directus/directus/pull/7115) update dependency knex to v0.95.9
([@renovate[bot]](https://github.com/apps/renovate))
- [#7110](https://github.com/directus/directus/pull/7110) update dependency sass to v1.37.0
([@renovate[bot]](https://github.com/apps/renovate))
- [#7109](https://github.com/directus/directus/pull/7109) update dependency eslint to v7.32.0
([@renovate[bot]](https://github.com/apps/renovate))
- [#7094](https://github.com/directus/directus/pull/7094) update dependency @rollup/plugin-commonjs to v20
([@renovate[bot]](https://github.com/apps/renovate))
- [#7093](https://github.com/directus/directus/pull/7093) update dependency chalk to v4.1.2
([@renovate[bot]](https://github.com/apps/renovate))
- [#7090](https://github.com/directus/directus/pull/7090) update dependency npm-watch to v0.11.0
([@renovate[bot]](https://github.com/apps/renovate))
- [#7089](https://github.com/directus/directus/pull/7089) update dependency eslint-plugin-vue to v7.15.0
([@renovate[bot]](https://github.com/apps/renovate))
- [#7087](https://github.com/directus/directus/pull/7087) update styfle/cancel-workflow-action action to v0.9.1
([@renovate[bot]](https://github.com/apps/renovate))
- [#7085](https://github.com/directus/directus/pull/7085) update dependency rollup to v2.55.1
([@renovate[bot]](https://github.com/apps/renovate))
## v9.0.0-rc.87 (July 28, 2021)

35
package-lock.json generated
View File

@@ -90,14 +90,13 @@
"dotenv": "^10.0.0",
"eventemitter2": "^6.4.3",
"execa": "^5.1.1",
"exif-reader": "^1.0.3",
"exifr": "^7.1.2",
"express": "^4.17.1",
"express-session": "^1.17.2",
"fs-extra": "^10.0.0",
"grant": "^5.4.14",
"graphql": "^15.5.0",
"graphql-compose": "^9.0.1",
"icc": "^2.0.0",
"inquirer": "^8.1.1",
"joi": "^17.3.0",
"js-yaml": "^4.1.0",
@@ -19872,10 +19871,10 @@
"node": ">=8"
}
},
"node_modules/exif-reader": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/exif-reader/-/exif-reader-1.0.3.tgz",
"integrity": "sha512-tWMBj1+9jUSibgR/kv/GQ/fkR0biaN9GEZ5iPdf7jFeH//d2bSzgPoaWf1OfMv4MXFD4upwvpCCyeMvSyLWSfA=="
"node_modules/exifr": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.2.tgz",
"integrity": "sha512-XZRvQJFJVeMfb6BLY755MoOBq+V644nzxRPjZ8G5+qvBi0nfcT1n2OdA4CshBKTF12fC1RQM9cpiKtQ/LwFLog=="
},
"node_modules/exit": {
"version": "0.1.2",
@@ -26541,14 +26540,6 @@
"node": ">=4"
}
},
"node_modules/icc": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/icc/-/icc-2.0.0.tgz",
"integrity": "sha512-VSTak7UAcZu1E24YFvcoHVpVg/ZUVyb0G1v0wUIibfz5mHvcFeI/Gpn8C0cAUKw5jCCGx5JBcV4gULu6hX97mA==",
"engines": {
"node": ">=10"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -75477,14 +75468,13 @@
"dotenv": "^10.0.0",
"eventemitter2": "^6.4.3",
"execa": "^5.1.1",
"exif-reader": "^1.0.3",
"exifr": "^7.1.2",
"express": "^4.17.1",
"express-session": "^1.17.2",
"fs-extra": "^10.0.0",
"grant": "^5.4.14",
"graphql": "^15.5.0",
"graphql-compose": "^9.0.1",
"icc": "^2.0.0",
"inquirer": "^8.1.1",
"ioredis": "^4.27.6",
"joi": "^17.3.0",
@@ -76951,10 +76941,10 @@
"clone-regexp": "^2.1.0"
}
},
"exif-reader": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/exif-reader/-/exif-reader-1.0.3.tgz",
"integrity": "sha512-tWMBj1+9jUSibgR/kv/GQ/fkR0biaN9GEZ5iPdf7jFeH//d2bSzgPoaWf1OfMv4MXFD4upwvpCCyeMvSyLWSfA=="
"exifr": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.2.tgz",
"integrity": "sha512-XZRvQJFJVeMfb6BLY755MoOBq+V644nzxRPjZ8G5+qvBi0nfcT1n2OdA4CshBKTF12fC1RQM9cpiKtQ/LwFLog=="
},
"exit": {
"version": "0.1.2",
@@ -82233,11 +82223,6 @@
"resolved": "https://registry.npmjs.org/hyperlinker/-/hyperlinker-1.0.0.tgz",
"integrity": "sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ=="
},
"icc": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/icc/-/icc-2.0.0.tgz",
"integrity": "sha512-VSTak7UAcZu1E24YFvcoHVpVg/ZUVyb0G1v0wUIibfz5mHvcFeI/Gpn8C0cAUKw5jCCGx5JBcV4gULu6hX97mA=="
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",