diff --git a/.babelrc b/.babelrc new file mode 100755 index 0000000..cca366f --- /dev/null +++ b/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + "env" + ], + "plugins": [ + "transform-object-rest-spread", + ["transform-runtime", { + "polyfill": false, + "regenerator": true + }] + ] +} diff --git a/.eslintignore b/.eslintignore new file mode 100755 index 0000000..4fcde7e --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +config/** diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100755 index 0000000..35fe6bb --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,268 @@ +module.exports = { + 'parser': 'babel-eslint', + 'env': { + 'browser': true, + 'es6': true, + 'jquery': true, + 'node': true, + 'mocha': true + }, + 'extends': 'eslint:recommended', + 'plugins': ['import'], + 'parserOptions': { + 'sourceType': 'module' + }, + 'globals': { + 'jpeg': true, + 'JpegImage': true, + 'OpenJPEG': true, + 'JpxImage': true, + 'CharLS': true, + 'pako': true + }, + 'rules': { + 'no-constant-condition': 'off', + 'accessor-pairs': 'warn', + 'array-bracket-spacing': 'warn', + 'array-callback-return': 'warn', + 'arrow-body-style': 'warn', + 'arrow-parens': 'warn', + 'arrow-spacing': 'warn', + 'block-scoped-var': 'warn', + 'block-spacing': 'warn', + 'brace-style': 'warn', + 'callback-return': 'warn', + 'camelcase': 'warn', + 'capitalized-comments': 'off', + 'class-methods-use-this': 'warn', + 'comma-dangle': 'warn', + 'comma-spacing': [ + 'warn', + { + 'after': true, + 'before': false + } + ], + 'comma-style': 'warn', + 'complexity': 'warn', + 'computed-property-spacing': 'warn', + 'consistent-return': 'warn', + 'consistent-this': 'warn', + 'curly': 'warn', + 'default-case': 'warn', + 'dot-location': 'warn', + 'dot-notation': 'warn', + 'eol-last': 'warn', + 'eqeqeq': 'warn', + 'func-call-spacing': 'warn', + 'func-name-matching': 'warn', + 'func-names': [ + 'warn', + 'never' + ], + 'func-style': ['warn', 'declaration'], + 'generator-star-spacing': 'warn', + 'global-require': 'warn', + 'guard-for-in': 'warn', + 'handle-callback-err': 'warn', + 'id-blacklist': 'warn', + 'id-length': 'off', + 'id-match': 'warn', + 'indent': ['warn', 2], + //'init-declarations': 'warn', + 'import/default': 'warn', + 'import/export': 'warn', + 'import/extensions': ['warn', { "js": "always" }], + 'import/first': 'warn', + 'import/named': 'warn', + 'import/namespace': 'warn', + 'import/newline-after-import': 'warn', + 'import/no-unresolved': 'warn', + 'import/no-webpack-loader-syntax': 'warn', + 'jsx-quotes': 'warn', + 'key-spacing': 'warn', + 'keyword-spacing': [ + 'warn', + { + 'after': true, + 'before': true + } + ], + 'line-comment-position': 'warn', + 'linebreak-style': [ + 'warn', + 'unix' + ], + 'lines-around-comment': 'warn', + 'lines-around-directive': 'warn', + 'max-depth': 'warn', + 'max-len': ['off', { + 'code': 120, + 'tabWidth': 2, + 'comments': 120, + 'ignoreComments': false, + 'ignoreTrailingComments': true, + 'ignoreUrls': true, + 'ignoreTemplateLiterals': false, + 'ignoreRegExpLiterals': true + }], + 'max-lines': 'warn', + 'max-nested-callbacks': 'warn', + 'max-params': ['warn', 7], + 'max-statements': ['warn', 40], + 'max-statements-per-line': 'warn', + 'multiline-ternary': 'off', + 'new-cap': 'warn', + 'new-parens': 'warn', + 'newline-after-var': 'warn', + 'newline-before-return': 'warn', + 'newline-per-chained-call': 'warn', + 'no-alert': 'warn', + 'no-array-constructor': 'warn', + 'no-bitwise': 'off', + 'no-caller': 'warn', + 'no-catch-shadow': 'warn', + 'no-confusing-arrow': 'warn', + 'no-console': 'off', + 'no-continue': 'warn', + 'no-div-regex': 'warn', + 'no-duplicate-imports': 'warn', + 'no-else-return': 'warn', + 'no-empty-function': 'off', + 'no-eq-null': 'warn', + 'no-eval': 'warn', + 'no-extend-native': 'warn', + 'no-extra-bind': 'warn', + 'no-extra-label': 'warn', + 'no-extra-parens': 'off', + 'no-floating-decimal': 'warn', + 'no-implicit-coercion': 'warn', + 'no-implicit-globals': 'warn', + 'no-implied-eval': 'warn', + 'no-inline-comments': 'warn', + 'no-invalid-this': 'off', + 'no-iterator': 'warn', + 'no-label-var': 'warn', + 'no-labels': 'warn', + 'no-lone-blocks': 'warn', + 'no-lonely-if': 'warn', + 'no-loop-func': 'warn', + 'no-magic-numbers': 'off', + 'no-mixed-operators': 'warn', + 'no-mixed-requires': 'warn', + 'no-multi-spaces': 'warn', + 'no-multi-str': 'warn', + 'no-multiple-empty-lines': 'warn', + 'no-native-reassign': 'warn', + 'no-negated-condition': 'warn', + 'no-negated-in-lhs': 'warn', + 'no-nested-ternary': 'warn', + 'no-new': 'warn', + 'no-new-func': 'warn', + 'no-new-object': 'warn', + 'no-new-require': 'warn', + 'no-new-wrappers': 'warn', + 'no-octal-escape': 'warn', + 'no-param-reassign': 'warn', + 'no-path-concat': 'warn', + 'no-plusplus': 'off', + 'no-process-env': 'warn', + 'no-process-exit': 'warn', + 'no-proto': 'warn', + 'no-prototype-builtins': 'warn', + 'no-restricted-globals': 'warn', + 'no-restricted-imports': 'warn', + 'no-restricted-modules': 'warn', + 'no-restricted-properties': 'warn', + 'no-restricted-syntax': 'warn', + 'no-return-assign': 'warn', + 'no-return-await': 'warn', + 'no-script-url': 'warn', + 'no-self-compare': 'warn', + 'no-sequences': 'warn', + 'no-shadow': 'warn', + 'no-shadow-restricted-names': 'warn', + 'no-spaced-func': 'warn', + 'no-sync': 'warn', + 'no-tabs': 'warn', + 'no-template-curly-in-string': 'warn', + 'no-ternary': 'off', + 'no-throw-literal': 'warn', + 'no-trailing-spaces': 'warn', + 'no-undef': 'error', + 'no-undef-init': 'warn', + 'no-undefined': 'off', + 'no-unused-vars': 'warn', + 'no-underscore-dangle': 'warn', + 'no-unmodified-loop-condition': 'warn', + 'no-unneeded-ternary': 'warn', + 'no-unused-expressions': 'warn', + 'no-use-before-define': ['warn', 'nofunc'], + 'no-useless-call': 'warn', + 'no-useless-computed-key': 'warn', + 'no-useless-concat': 'warn', + 'no-useless-constructor': 'warn', + 'no-useless-escape': 'warn', + 'no-useless-rename': 'warn', + 'no-useless-return': 'warn', + 'no-var': 'off', + 'no-void': 'off', + 'no-warning-comments': 'warn', + 'no-whitespace-before-property': 'warn', + 'no-with': 'warn', + 'object-curly-spacing': [ + 'warn', + 'always' + ], + 'object-property-newline': 'warn', + 'object-shorthand': 'warn', + 'one-var': 'off', + 'one-var-declaration-per-line': 'warn', + 'operator-assignment': 'warn', + 'operator-linebreak': 'warn', + 'padded-blocks': ['warn', 'never'], + 'prefer-arrow-callback': 'off', + 'prefer-const': 'warn', + 'prefer-numeric-literals': 'warn', + 'prefer-reflect': 'warn', + 'prefer-rest-params': 'warn', + 'prefer-spread': 'warn', + 'prefer-template': 'warn', + 'quote-props': ['warn', 'as-needed'], + 'quotes': [ + 'warn', + 'single' + ], + 'radix': 'warn', + 'require-await': 'warn', + //'require-jsdoc': 'warn', + 'rest-spread-spacing': 'warn', + 'semi': 'warn', + 'semi-spacing': 'warn', + 'sort-imports': 'off', + 'sort-keys': 'off', + 'sort-vars': 'off', + 'space-before-blocks': 'warn', + 'space-before-function-paren': 'warn', + 'space-in-parens': [ + 'warn', + 'never' + ], + 'space-infix-ops': 'warn', + 'space-unary-ops': 'warn', + 'spaced-comment': 'warn', + 'strict': 'warn', + 'symbol-description': 'warn', + 'template-curly-spacing': 'warn', + 'unicode-bom': [ + 'warn', + 'never' + ], + //'valid-jsdoc': 'warn', + 'vars-on-top': 'warn', + 'wrap-iife': ['warn', 'inside'], + 'wrap-regex': 'warn', + 'yield-star-spacing': 'warn', + 'yoda': 'warn' + } +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07b9584 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +## Directories +node_modules/ +coverage/ +dist/ + +## Files +npm-debug.log +.idea +.DS_Store \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100755 index 0000000..bb88ae9 --- /dev/null +++ b/.npmignore @@ -0,0 +1,11 @@ +## All non-dist directories +config/ +coverage/ +src/ +test/ + +## Root files +.babelrc +.eslintignore +.gitignore +.travis.yml \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100755 index 0000000..431e6e2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: node_js + +addons: + chrome: stable + +node_js: + - "node" + +cache: + yarn: true + directories: + - node_modules + +after_success: + cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..1a4df21 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Radialogica, LLC (bryan@radialogica.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b36aade --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +[![npm version](https://badge.fury.io/js/dicom-character-set.svg)](https://badge.fury.io/js/dicom-character-set) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Build Status](https://travis-ci.org/radialogica/dicom-character-set.svg?branch=master)][https://travis-ci.org/radialogica/dicom-character-set] +[![Coverage Status](https://coveralls.io/repos/github/radialogica/dicom-character-set/badge.svg?branch=master)][https://coveralls.io/github/radialogica/dicom-character-set] + +dicom-character-set +=================== +Converts DICOM text (as bytes) to a JavaScript string. Handles multiple character sets (single-byte and multi-byte, with and without extensions) within a single block of text according to the DICOM standard. All encodings specified in the standard are currently supported. For a complete list of all encodings, see [here](http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2). + +Install +------- + +Install via [NPM](https://www.npmjs.com/): + +`npm install dicom-parser` + +Or get a packaged source file: + +* [dicom-character-set.js](https://unpkg.com/dicom-character-set@latest/dist/dicom-character-set.js) +* [dicom-character-set.min.js](https://unpkg.com/dicom-character-set@latest/dist/dicom-character-set.min.js) + +Usage +----- +Firefox/Chrome/Safari: +``` +import { convertBytes } from 'dicom-character-set'; +const str = convertBytes('ISO 2022 IR 149\\ISO 2022 IR 13', uint8ArrayBytes, {vr: 'LT'}); +``` +Internet Explorer (doesn't support TextDecoder so you have to use promises) : +``` +import { convertBytesPromise } from 'dicom-character-set'; + +convertBytesPromise('ISO 2022 IR 6\\ISO 2022 IR 13', uint8ArrayBytes, {vr: 'LT'}).then(str => { + console.log(str); +}); +``` +Note: Make sure you're passing the text as a Uint8Array, not as a string! Also, only pass the bytes of the value you want converted, not the bytes for the entire DICOM file. + +Arguments +------- +Both convertBytes and convertBytesPromise take the same arguments. They are, in order: +* Specific Character Set attribute value (0008,0005) from the DICOM file +* Text bytes as Uint8Array +* Options object (optional). Supported options are: + * **vr** (string) : the value representation of the text being converted. Gives the decoder a hint for properly handling delimiters. If not specified, the decoder assumes backslash, carriage return, line feed, form feed, and tab all reset the active character set to the first one specified (see [the standard](http://dicom.nema.org/medical/dicom/current/output/html/part05.html#sect_6.1.2.5.3) for details). + +Differences from DICOM standard +------------------------------- +In the name of robustness, the behavior varies from the standard DICOM in the following ways: +* If one of the multi-byte character sets not supporting extensions (e.g. GBK) appears first, all following character sets will be ignored; if it appears after any other character set, it will be ignored. +* If multiple character sets are specified, the non-extension character sets are switched to their extension equivalents where applicable (i.e. "ISO_IR 100\ISO_IR 101" would become "ISO 2022 IR 100\ISO 2022 IR 101") +* Control characters (in the CL and CR planes) are allowed, though they probably won't print much +* A multi-byte character set supporting code extensions can be the first character set +* If the same character set appears multiple times, ignore any duplicate occurrences +* If a character is encountered in a code element that hasn't been assigned, it's printed using the currently active code element instead of throwing an error \ No newline at end of file diff --git a/config/karma/karma-base.js b/config/karma/karma-base.js new file mode 100755 index 0000000..5769d9e --- /dev/null +++ b/config/karma/karma-base.js @@ -0,0 +1,62 @@ +const path = require('path'); +const webpackConfig = require('../webpack'); + +/* eslint no-process-env:0 */ +process.env.CHROME_BIN = require('puppeteer').executablePath(); + +// Deleting output.library to avoid "Uncaught SyntaxError: Unexpected token /" error +// when running tests (var test/foo_test.js = ...) +delete webpackConfig.output.library; + +// Code coverage +webpackConfig.module.rules.push({ + test: /\.js$/, + include: path.resolve('./src/'), + loader: 'istanbul-instrumenter-loader', + query: { + esModules: true + } +}); + +module.exports = { + basePath: '../../', + frameworks: ['mocha'], + reporters: ['progress', 'coverage'], + files: [ + 'test/**/*-test.js' + ], + + plugins: [ + 'karma-webpack', + 'karma-mocha', + 'karma-chrome-launcher', + 'karma-firefox-launcher', + 'karma-coverage' + ], + + preprocessors: { + 'src/**/*.js': ['webpack'], + 'test/**/*-test.js': ['webpack'] + }, + + webpack: webpackConfig, + + webpackMiddleware: { + noInfo: false, + stats: { + chunks: false, + timings: false, + errorDetails: true + } + }, + + coverageReporter: { + dir: './coverage', + reporters: [ + {type: 'html', subdir: 'html'}, + {type: 'lcov', subdir: '.'}, + {type: 'text', subdir: '.', file: 'text.txt'}, + {type: 'text-summary', subdir: '.', file: 'text-summary.txt'} + ] + } +}; \ No newline at end of file diff --git a/config/karma/karma-chrome.js b/config/karma/karma-chrome.js new file mode 100755 index 0000000..79e69a8 --- /dev/null +++ b/config/karma/karma-chrome.js @@ -0,0 +1,15 @@ +const extendConfiguration = require('./karma-extend.js'); + +module.exports = function (config) { + 'use strict'; + config.set(extendConfiguration({ + singleRun: true, + browsers: ['ChromeHeadlessNoSandbox'], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: 'ChromeHeadless', + flags: ['--no-sandbox'] + } + } + })); +}; diff --git a/config/karma/karma-coverage.js b/config/karma/karma-coverage.js new file mode 100755 index 0000000..e2ed2ab --- /dev/null +++ b/config/karma/karma-coverage.js @@ -0,0 +1,16 @@ +const extendConfiguration = require('./karma-extend.js'); + +module.exports = function (config) { + 'use strict'; + config.set(extendConfiguration({ + singleRun: true, + reporters: ['progress', 'coverage'], + browsers: ['ChromeHeadlessNoSandbox'], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: 'ChromeHeadless', + flags: ['--no-sandbox'] + } + } + })); +}; diff --git a/config/karma/karma-coveralls.js b/config/karma/karma-coveralls.js new file mode 100755 index 0000000..059408d --- /dev/null +++ b/config/karma/karma-coveralls.js @@ -0,0 +1,16 @@ +const extendConfiguration = require('./karma-extend.js'); + +module.exports = function (config) { + 'use strict'; + config.set(extendConfiguration({ + singleRun: true, + reporters: ['progress', 'coverage', 'coveralls'], + browsers: ['ChromeHeadlessNoSandbox'], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: 'ChromeHeadless', + flags: ['--no-sandbox'] + } + } + })); +}; diff --git a/config/karma/karma-extend.js b/config/karma/karma-extend.js new file mode 100755 index 0000000..f4ca2a4 --- /dev/null +++ b/config/karma/karma-extend.js @@ -0,0 +1,12 @@ +const baseConfig = require('./karma-base.js'); + +module.exports = function (extendedConfig) { + 'use strict'; + // Overrides the base configuration for karma with the given properties + for (var i in baseConfig) { + if (typeof extendedConfig[i] === 'undefined') { + extendedConfig[i] = baseConfig[i]; + } + } + return extendedConfig; +}; diff --git a/config/karma/karma-firefox.js b/config/karma/karma-firefox.js new file mode 100755 index 0000000..7d9ff67 --- /dev/null +++ b/config/karma/karma-firefox.js @@ -0,0 +1,9 @@ +const extendConfiguration = require('./karma-extend.js'); + +module.exports = function (config) { + 'use strict'; + config.set(extendConfiguration({ + singleRun: true, + browsers: ['Firefox'] + })); +}; diff --git a/config/karma/karma-watch.js b/config/karma/karma-watch.js new file mode 100755 index 0000000..e5256b3 --- /dev/null +++ b/config/karma/karma-watch.js @@ -0,0 +1,14 @@ +const extendConfiguration = require('./karma-extend.js'); + +module.exports = function (config) { + 'use strict'; + config.set(extendConfiguration({ + browsers: ['ChromeHeadlessNoSandbox'], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: 'ChromeHeadless', + flags: ['--no-sandbox'] + } + } + })); +}; diff --git a/config/webpack/index.js b/config/webpack/index.js new file mode 100755 index 0000000..91f29de --- /dev/null +++ b/config/webpack/index.js @@ -0,0 +1,5 @@ +const path = require('path'); +const env = process.env.ENV || 'dev'; +const config = require(`./webpack-${env}`); + +module.exports = config; diff --git a/config/webpack/merge.js b/config/webpack/merge.js new file mode 100755 index 0000000..1c754ac --- /dev/null +++ b/config/webpack/merge.js @@ -0,0 +1,15 @@ +const _ = require('lodash'); + +// Merge two objects +// Instead of merging array objects index by index (n-th source +// item with n-th object item) it concatenates both arrays +module.exports = function(object, source) { + const clone = _.cloneDeep(object); + const merged = _.mergeWith(clone, source, function(objValue, srcValue, key, object, source, stack) { + if(objValue && srcValue && _.isArray(objValue) && _.isArray(srcValue)) { + return _.concat(objValue, srcValue); + } + }); + + return merged; +} \ No newline at end of file diff --git a/config/webpack/plugins/banner.js b/config/webpack/plugins/banner.js new file mode 100755 index 0000000..397a5e3 --- /dev/null +++ b/config/webpack/plugins/banner.js @@ -0,0 +1,28 @@ +const path = require('path'); +const webpack = require('webpack'); +const rootPath = process.cwd(); +const pkgPath = path.join(rootPath, "package"); +const pkg = require(pkgPath); + +const getCurrentDate = () => { + const today = new Date(); + const year = today.getFullYear(); + const month = ('0' + (today.getMonth() + 1)).slice(-2); + const date = ('0' + today.getDate()).slice(-2); + + return `${year}-${month}-${date}`; +} + +const getBanner = () => { + return `/*! ${pkg.name} - ${pkg.version} - ` + + `${getCurrentDate()} ` + + `| (c) 2018 Radialogica, LLC | ${pkg.homepage} */` +} + +module.exports = () => { + return new webpack.BannerPlugin({ + banner: getBanner(), + entryOnly: true, + raw: true + }); +} diff --git a/config/webpack/webpack-base.js b/config/webpack/webpack-base.js new file mode 100755 index 0000000..4fd6ad4 --- /dev/null +++ b/config/webpack/webpack-base.js @@ -0,0 +1,47 @@ +const path = require('path'); +const rootPath = process.cwd(); +const context = path.join(rootPath, "src"); +const outputPath = path.join(rootPath, 'dist'); +const bannerPlugin = require(path.join(__dirname, 'plugins', 'banner.js')); + +module.exports = { + mode: 'development', + context: context, + entry: { + 'dicom-character-set': './index.js' + }, + target: 'web', + output: { + filename: '[name].js', + library: { + commonjs: "dicom-character-set", + amd: "dicom-character-set", + root: 'dicom-character-set' + }, + libraryTarget: 'umd', + globalObject: 'this', + path: outputPath, + umdNamedDefine: true + }, + devtool: 'source-map', + module: { + rules: [{ + enforce: 'pre', + test: /\.js$/, + exclude: /(node_modules|test)/, + loader: 'eslint-loader', + options: { + failOnError: false + } + }, { + test: /\.js$/, + exclude: /(node_modules)/, + use: [{ + loader: 'babel-loader' + }] + }] + }, + plugins: [ + bannerPlugin() + ] +}; diff --git a/config/webpack/webpack-dev.js b/config/webpack/webpack-dev.js new file mode 100755 index 0000000..481e9d4 --- /dev/null +++ b/config/webpack/webpack-dev.js @@ -0,0 +1 @@ +module.exports = require('./webpack-base'); diff --git a/config/webpack/webpack-prod.js b/config/webpack/webpack-prod.js new file mode 100755 index 0000000..b63fc6b --- /dev/null +++ b/config/webpack/webpack-prod.js @@ -0,0 +1,19 @@ +const merge = require('./merge'); +const baseConfig = require('./webpack-base'); +const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); + +const devConfig = { + output: { + filename: '[name].min.js' + }, + mode: "production", + optimization: { + minimizer: [ + new UglifyJSPlugin({ + sourceMap: true + }) + ] + }, +}; + +module.exports = merge(baseConfig, devConfig); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..1ab5390 --- /dev/null +++ b/package.json @@ -0,0 +1,75 @@ +{ + "name": "dicom-character-set", + "version": "1.0.0", + "description": "Converts bytes of encoded DICOM text to Javascript DOMString", + "main": "dist/dicom-character-set.min.js", + "module": "dist/dicom-character-set.min.js", + "keywords": [ + "DICOM", + "medical", + "imaging", + "character", + "encoding" + ], + "author": "Bryan Cool", + "homepage": "https://github.com/radialogica/dicom-character-set", + "license": "MIT", + "bugs": { + "url": "https://github.com/radialogica/dicom-character-set" + }, + "repository": { + "type": "git", + "url": "https://github.com/radialogica/dicom-character-set.git" + }, + "scripts": { + "build": "npm run test && npm run version && npm run webpack", + "clean": "npm run clean:dist && npm run clean:coverage", + "clean:coverage": "shx rm -rf coverage", + "clean:dist": "shx rm -rf dist", + "eslint": "eslint -c .eslintrc.js src", + "eslint-fix": "eslint -c .eslintrc.js --fix src", + "eslint-quiet": "eslint -c .eslintrc.js --quiet src", + "start": "npm run webpack", + "test": "npm run test:chrome", + "test:all": "npm run test:chrome && npm run test:firefox", + "test:chrome": "karma start config/karma/karma-chrome.js", + "test:firefox": "karma start config/karma/karma-firefox.js", + "test:watch": "karma start config/karma/karma-watch.js", + "version": "node -p -e \"'export default \\'' + require('./package.json').version + '\\';'\" > src/version.js", + "watch": "npm run webpack:watch", + "webpack": "npm run clean:dist && npm run webpack:prod && npm run webpack:dev", + "webpack:dev": "webpack --progress --config ./config/webpack/webpack-dev", + "webpack:prod": "webpack --progress --config ./config/webpack/webpack-prod", + "webpack:watch": "webpack --progress --debug --watch --config ./config/webpack" + }, + "devDependencies": { + "babel-core": "^6.26.0", + "babel-eslint": "^8.0.1", + "babel-loader": "^7.1.2", + "babel-plugin-transform-object-rest-spread": "^6.26.0", + "babel-plugin-transform-runtime": "^6.23.0", + "babel-preset-env": "^1.7.0", + "chai": "^4.1.2", + "coveralls": "^3.0.0", + "eslint": "^4.10.0", + "eslint-loader": "^2.0.0", + "eslint-plugin-import": "^2.8.0", + "istanbul": "^0.4.5", + "istanbul-instrumenter-loader": "^3.0.0", + "karma": "^1.7.1", + "karma-chrome-launcher": "^2.2.0", + "karma-coverage": "^1.1.1", + "karma-firefox-launcher": "^1.0.1", + "karma-mocha": "^1.3.0", + "karma-webpack": "^3.0.0", + "lodash": "4.17.4", + "mocha": "^4.0.1", + "opn-cli": "^3.1.0", + "puppeteer": "^1.2.0", + "rusha": "^0.8.6", + "shx": "^0.2.2", + "uglifyjs-webpack-plugin": "^1.2.4", + "webpack": "^4.4.1", + "webpack-cli": "^2.0.13" + } +} diff --git a/src/character-sets.js b/src/character-sets.js new file mode 100644 index 0000000..902fdab --- /dev/null +++ b/src/character-sets.js @@ -0,0 +1,221 @@ +const asciiElement = { codeElement: 'G0', + escapeSequence: [0x1B, 0x28, 0x42], + encoding: 'windows-1254', + isASCII: true, + bytesPerCodePoint: 1 }; + +const characterSets = { + + /** ******************************** + * Single-byte without extensions * + **********************************/ + + // Default + 'ISO_IR 6': { encoding: 'utf-8' }, + + // Latin alphabet No. 1 + 'ISO_IR 100': { encoding: 'windows-1254' }, + + // Latin alphabet No. 2 + 'ISO_IR 101': { encoding: 'iso-8859-2' }, + + // Latin alphabet No. 3 + 'ISO_IR 109': { encoding: 'iso-8859-3' }, + + // Latin alphabet No. 4 + 'ISO_IR 110': { encoding: 'iso-8859-4' }, + + // Cyrillic + 'ISO_IR 144': { encoding: 'iso-8859-5' }, + + // Arabic + 'ISO_IR 127': { encoding: 'iso-8859-6' }, + + // Greek + 'ISO_IR 126': { encoding: 'iso-8859-7' }, + + // Hebrew + 'ISO_IR 138': { encoding: 'iso-8859-8' }, + + // Latin alphabet No. 5 + 'ISO_IR 148': { encoding: 'windows-1254' }, + + // Japanese + 'ISO_IR 13': { encoding: 'shift-jis' }, + + // Thai + 'ISO_IR 166': { encoding: 'tis-620' }, + + /** ***************************** + * Single-byte with extensions * + *******************************/ + + // Default + 'ISO 2022 IR 6': { + extension: true, + elements: [asciiElement] + }, + + // Latin alphabet No. 1 + 'ISO 2022 IR 100': { + extension: true, + elements: [asciiElement, { codeElement: 'G1', + escapeSequence: [0x1B, 0x2D, 0x41], + encoding: 'windows-1254', + bytesPerCodePoint: 1 }] + }, + + // Latin alphabet No. 2 + 'ISO 2022 IR 101': { + extension: true, + elements: [asciiElement, { codeElement: 'G1', + escapeSequence: [0x1B, 0x2D, 0x42], + encoding: 'iso-8859-2', + bytesPerCodePoint: 1 }] + }, + + // Latin alphabet No. 3 + 'ISO 2022 IR 109': { + extension: true, + elements: [asciiElement, { codeElement: 'G1', + escapeSequence: [0x1B, 0x2D, 0x43], + encoding: 'iso-8859-3', + bytesPerCodePoint: 1 }] + }, + + // Latin alphabet No. 4 + 'ISO 2022 IR 110': { + extension: true, + elements: [asciiElement, { codeElement: 'G1', + escapeSequence: [0x1B, 0x2D, 0x44], + encoding: 'iso-8859-4', + bytesPerCodePoint: 1 }] + }, + + // Cyrillic + 'ISO 2022 IR 144': { + extension: true, + elements: [asciiElement, { codeElement: 'G1', + escapeSequence: [0x1B, 0x2D, 0x4C], + encoding: 'iso-8859-5', + bytesPerCodePoint: 1 }] + }, + + // Arabic + 'ISO 2022 IR 127': { + extension: true, + elements: [asciiElement, { codeElement: 'G1', + escapeSequence: [0x1B, 0x2D, 0x47], + encoding: 'iso-8859-6', + bytesPerCodePoint: 1 }] + }, + + // Greek + 'ISO 2022 IR 126': { + extension: true, + elements: [asciiElement, { codeElement: 'G1', + escapeSequence: [0x1B, 0x2D, 0x46], + encoding: 'iso-8859-7', + bytesPerCodePoint: 1 }] + }, + + // Hebrew + 'ISO 2022 IR 138': { + extension: true, + elements: [asciiElement, { codeElement: 'G1', + escapeSequence: [0x1B, 0x2D, 0x48], + encoding: 'iso-8859-8', + bytesPerCodePoint: 1 }] + }, + + // Latin alphabet No. 5 + 'ISO 2022 IR 148': { + extension: true, + elements: [asciiElement, { codeElement: 'G1', + escapeSequence: [0x1B, 0x2D, 0x4D], + encoding: 'windows-1254', + bytesPerCodePoint: 1 }] + }, + + // Japanese + 'ISO 2022 IR 13': { + extension: true, + elements: [{ codeElement: 'G0', + escapeSequence: [0x1B, 0x28, 0x4A], + encoding: 'shift-jis', + bytesPerCodePoint: 1 }, + { codeElement: 'G1', + escapeSequence: [0x1B, 0x29, 0x49], + encoding: 'shift-jis', + bytesPerCodePoint: 1 }] + }, + + // Thai + 'ISO 2022 IR 166': { + extension: true, + elements: [asciiElement, { codeElement: 'G1', + escapeSequence: [0x1B, 0x2D, 0x54], + encoding: 'tis-620', + bytesPerCodePoint: 1 }] + }, + + /** **************************** + * Multi-byte with extensions * + ******************************/ + + // Japanese + 'ISO 2022 IR 87': { + extension: true, + multiByte: true, + elements: [{ codeElement: 'G0', + escapeSequence: [0x1B, 0x24, 0x42], + encoding: 'euc-jp', + setHighBit: true, + bytesPerCodePoint: 2 }] + }, + + 'ISO 2022 IR 159': { + extension: true, + multiByte: true, + elements: [{ codeElement: 'G0', + escapeSequence: [0x1B, 0x24, 0x28, 0x44], + encoding: 'euc-jp', + isJISX0212: true, + bytesPerCodePoint: 2 }] + }, + + // Korean + 'ISO 2022 IR 149': { + extension: true, + multiByte: true, + elements: [{ codeElement: 'G1', + escapeSequence: [0x1B, 0x24, 0x29, 0x43], + encoding: 'euc-kr', + bytesPerCodePoint: 2 }] + }, + + // Simplified Chinese + 'ISO 2022 IR 58': { + extension: true, + multiByte: true, + elements: [{ codeElement: 'G1', + escapeSequence: [0x1B, 0x24, 0x29, 0x41], + encoding: 'gb18030', + bytesPerCodePoint: 2 }] + }, + + /** ******************************* + * Multi-byte without extensions * + *********************************/ + + 'ISO_IR 192': { encoding: 'utf-8', + multiByte: true }, + + GB18030: { encoding: 'gb18030', + multiByte: true }, + + GBK: { encoding: 'gbk', + multiByte: true } +}; + +export default characterSets; diff --git a/src/convert-bytes.js b/src/convert-bytes.js new file mode 100644 index 0000000..97d2942 --- /dev/null +++ b/src/convert-bytes.js @@ -0,0 +1,294 @@ +import characterSets from './character-sets.js'; + +const ESCAPE_BYTE = 0x1B; + +const CARRIAGE_RETURN = 0xA; +const LINE_FEED = 0xC; +const FORM_FEED = 0xD; +const TAB = 0x9; +// Aka yen symbol in Romaji +const BACKSLASH = 0x5C; +const EQUAL_SIGN = 0x3D; +const CARET = 0x5E; + + +function adjustShiftJISResult (str) { + // browsers do strict ASCII for these characters, so to be compliant with Shift JIS we replace them + return str.replace(/~/g, '‾').replace(/\\/g, '¥'); +} + +function appendRunWithoutPromise (output, byteRunCharacterSet, bytes, byteRunStart, byteRunEnd) { + const oneRunBytes = preprocessBytes(byteRunCharacterSet, bytes, byteRunStart, byteRunEnd); + + + return output + convertWithoutExtensions(byteRunCharacterSet.encoding, oneRunBytes); +} + +async function appendRunWithPromise (output, byteRunCharacterSet, bytes, byteRunStart, byteRunEnd) { + const oneRunBytes = preprocessBytes(byteRunCharacterSet, bytes, byteRunStart, byteRunEnd); + + + return (await output) + (await convertWithoutExtensionsPromise(byteRunCharacterSet.encoding, oneRunBytes)); +} + +function checkParameters (specificCharacterSet, bytes) { + if (bytes && !(bytes instanceof Uint8Array)) { + throw new Error('bytes must be a Uint8Array'); + } + if (specificCharacterSet && (typeof specificCharacterSet !== 'string')) { + throw new Error('specificCharacterSet must be a string'); + } +} + +function convertBytesCore (withoutExtensionsFunc, appendFunc, specificCharacterSet, bytes, options) { + checkParameters(specificCharacterSet, bytes); + + const characterSetStrings = getCharacterSetStrings(specificCharacterSet); + + if (characterSetStrings.length === 1 && !characterSetStrings[0].startsWith('ISO 2022')) { + return withoutExtensionsFunc(characterSets[characterSetStrings[0]].encoding, bytes); + } + + const checkedOptions = options || {}; + + return convertWithExtensions(characterSetStrings.map((characterSet) => characterSets[characterSet]), + bytes, getDelimitersForVR(checkedOptions.vr), appendFunc); +} + +function convertWithExtensions (allowedCharacterSets, bytes, delimiters, appendRun) { + let output = ''; + + if (!bytes || bytes.length === 0) { + return output; + } + + const initialCharacterSets = { + G0: allowedCharacterSets[0].elements.find((element) => element.codeElement === 'G0'), + G1: allowedCharacterSets[0].elements.find((element) => element.codeElement === 'G1') + }; + + const activeCharacterSets = Object.assign({}, initialCharacterSets); + let byteRunStart = 0; + let byteRunCharacterSet; + let nextSetIndex = 0; + + // Group bytes into runs based on their encoding so we don't have to use a different + // decoder for each character. Note that G0 and G1 planes can be different encodings, + // so we can't just group by character set. + + while (nextSetIndex < bytes.length) { + if (!byteRunCharacterSet) { + byteRunCharacterSet = getCharacterSet(bytes[byteRunStart], activeCharacterSets); + } + + const next = findNextCharacterSet(bytes, byteRunStart, byteRunCharacterSet, + activeCharacterSets, initialCharacterSets, delimiters); + + nextSetIndex = next.index; + + if (nextSetIndex > byteRunStart) { + output = appendRun(output, byteRunCharacterSet, bytes, byteRunStart, nextSetIndex); + } + + byteRunStart = nextSetIndex; + byteRunCharacterSet = next.characterSet; + + if (next.escapeSequence) { + const nextCharacterSet = readEscapeSequence(bytes, nextSetIndex, allowedCharacterSets); + + activeCharacterSets[nextCharacterSet.codeElement] = nextCharacterSet; + byteRunStart += nextCharacterSet.escapeSequence.length; + } + } + + return output; +} + +function convertWithoutExtensions (encoding, bytes) { + const retVal = new TextDecoder(encoding).decode(bytes); + + + return (encoding === 'shift-jis') ? adjustShiftJISResult(retVal) : retVal; +} + +function convertWithoutExtensionsPromise (encoding, bytes) { + return new Promise((resolve) => { + const fileReader = new FileReader(); + + if (encoding === 'shift-jis') { + fileReader.onload = () => resolve(adjustShiftJISResult(fileReader.result)); + } else { + fileReader.onload = () => resolve(fileReader.result); + } + + const blob = new Blob([bytes]); + + fileReader.readAsText(blob, encoding); + }); +} + +// Multibyte non-extension character sets must stand on their own or else be ignored. This method enforces that. +function filterMultiByteCharacterSetStrings (characterSetStrings) { + const initialCharacterSet = characterSets[characterSetStrings[0]]; + + if (initialCharacterSet.multiByte && !initialCharacterSet.extension) { + return [characterSetStrings[0]]; + } + + return characterSetStrings.filter((str) => !characterSets[str].multiByte || characterSets[str].extension); +} + +function findNextCharacterSet (bytes, start, currentCodeElement, activeCodeElements, initialCharacterSets, delimiters) { + for (let i = start; i < bytes.length; i += currentCodeElement.bytesPerCodePoint) { + if (bytes[i] === ESCAPE_BYTE) { + return { escapeSequence: true, + index: i }; + } + if (currentCodeElement.bytesPerCodePoint === 1 && delimiters.includes(bytes[i])) { + Object.assign(activeCodeElements, initialCharacterSets); + } + const nextCodeElement = getCharacterSet(bytes[i], activeCodeElements); + + if (currentCodeElement && nextCodeElement !== currentCodeElement) { + return { characterSet: nextCodeElement, + index: i }; + } + } + + return { index: bytes.length }; +} + +function forceExtensionsIfApplicable (characterSetStrings) { + const forceExtensions = (characterSetStrings.length > 1); + + const returnValue = []; + + for (const characterSetString of characterSetStrings) { + if (!returnValue.includes(characterSetString)) { + returnValue.push(forceExtensions ? characterSetString.replace('ISO_IR', 'ISO 2022 IR') : characterSetString); + } + } + + return returnValue; +} + +function getCharacterSet (byte, activeCharacterSets) { + if (byte > 0x7F && activeCharacterSets.G1) { + return activeCharacterSets.G1; + } + if (activeCharacterSets.G0) { + return activeCharacterSets.G0; + } + // for robustness if byte <= 0x7F, try to output using G1 if no G0 is selected + if (activeCharacterSets.G1 && activeCharacterSets.G1.bytesPerCodePoint === 1) { + return activeCharacterSets.G1; + } + // If G1 is multibyte, default to ASCII + + return characterSets['ISO 2022 IR 6'].elements[0]; +} + +function getCharacterSetStrings (specificCharacterSet) { + let characterSetStrings = specificCharacterSet ? specificCharacterSet.split('\\').map((characterSet) => characterSet.trim().toUpperCase()) : ['']; + + if (characterSetStrings[0] === '') { + characterSetStrings[0] = (characterSetStrings.length > 1) ? 'ISO 2022 IR 6' : 'ISO_IR 6'; + } + + if (characterSetStrings.some((characterSet) => characterSets[characterSet] === undefined)) { + throw new Error('Invalid specific character set specified.'); + } + + characterSetStrings = filterMultiByteCharacterSetStrings(characterSetStrings); + + return forceExtensionsIfApplicable(characterSetStrings); +} + +function getDelimitersForVR (incomingVR) { + const vr = (incomingVR || '').trim().toUpperCase(); + + const delimiters = [CARRIAGE_RETURN, LINE_FEED, FORM_FEED, TAB]; + + if (!['UT', 'ST', 'LT'].includes(vr)) { + // for delimiting multi-valued items + delimiters.push(BACKSLASH); + } + if (vr === 'PN') { + delimiters.push(EQUAL_SIGN); + delimiters.push(CARET); + } + + return delimiters; +} + +function preprocessBytes (characterSet, bytes, byteStart, byteEnd) { + let oneEncodingBytes; + + if (characterSet.isJISX0212) { + oneEncodingBytes = processJISX0212(bytes, byteStart, byteEnd); + } else { + oneEncodingBytes = bytes.slice(byteStart, byteEnd); + if (characterSet.setHighBit) { + setHighBit(oneEncodingBytes); + } + } + + return oneEncodingBytes; +} + +function processJISX0212 (bytes, bytesStart, bytesEnd) { + const length = bytesEnd - bytesStart; + + if (length % 2 !== 0) { + throw new Error('JIS X string with a character not having exactly two bytes!'); + } + + const processedBytes = new Uint8Array(length + (length / 2)); + let outIndex = 0; + + for (let i = bytesStart; i < bytesEnd; i += 2) { + processedBytes[outIndex++] = 0x8F; + processedBytes[outIndex++] = bytes[i] | 0x80; + processedBytes[outIndex++] = bytes[i + 1] | 0x80; + } + + return processedBytes; +} + +function escapeSequenceMatches (escapeSequence, bytes, startIndex) { + for (let escapeByteIndex = 0; escapeByteIndex < escapeSequence.length; escapeByteIndex++) { + if (startIndex + escapeByteIndex >= bytes.length) { + return false; + } else if (bytes[startIndex + escapeByteIndex] !== escapeSequence[escapeByteIndex]) { + return false; + } + } + + return true; +} + +function readEscapeSequence (bytes, start, extensionSets) { + for (const extensionSet of extensionSets) { + for (const element of extensionSet.elements) { + if (escapeSequenceMatches(element.escapeSequence, bytes, start)) { + return element; + } + } + } + + throw new Error(`Unknown escape sequence encountered at byte ${start}`); +} + +function setHighBit (bytes) { + for (let i = 0; i < bytes.length; i++) { + bytes[i] |= 0x80; + } +} + +export function convertBytes (specificCharacterSet, bytes, options) { + return convertBytesCore(convertWithoutExtensions, appendRunWithoutPromise, specificCharacterSet, bytes, options); +} + +export function convertBytesPromise (specificCharacterSet, bytes, options) { + return convertBytesCore(convertWithoutExtensionsPromise, appendRunWithPromise, specificCharacterSet, bytes, options); +} diff --git a/src/index.js b/src/index.js new file mode 100755 index 0000000..87da449 --- /dev/null +++ b/src/index.js @@ -0,0 +1 @@ +export { convertBytes, convertBytesPromise } from './convert-bytes.js'; diff --git a/src/version.js b/src/version.js new file mode 100644 index 0000000..3d64381 --- /dev/null +++ b/src/version.js @@ -0,0 +1 @@ +export default '1.0.0'; diff --git a/test/convert-bytes-test.js b/test/convert-bytes-test.js new file mode 100755 index 0000000..cb3704c --- /dev/null +++ b/test/convert-bytes-test.js @@ -0,0 +1,611 @@ +import { expect } from 'chai'; +import { convertBytes, convertBytesPromise } from '../src/convert-bytes.js'; +import characterSets from '../src/character-sets.js'; + +const examples = { + // Single byte (with/without extensions) + 'IR 6': {bytes: [0x7E, 0x20, 0x9, 0x21, 0x5C], value: '~ \t!\\'}, + 'IR 100': {bytes: [0x83, 0xD8, 0xF7, 0xFF], value: 'ƒØ÷ÿ'}, + 'IR 101': {bytes: [0xD8, 0xB8, 0xFF], value: 'ظ˙'}, + 'IR 109': {bytes: [0xA7, 0xA1, 0xF5], value: '§Ħġ'}, + 'IR 110': {bytes: [0xF1, 0xC6, 0xE6], value: 'ņÆæ'}, + 'IR 144': {bytes: [0xB6, 0xFD, 0xFF], value: 'Ж§џ'}, + 'IR 127': {bytes: [0xD4, 0xC1, 0xF2], value: 'شءْ'}, + 'IR 126': {bytes: [0xA5, 0xD8, 0xFE], value: '₯Ψώ'}, + 'IR 138': {bytes: [0xE4, 0xAE, 0xFA], value: 'ה®ת'}, + 'IR 148': {bytes: [0xD0, 0xDE, 0xFE], value: 'ĞŞş'}, + 'IR 13': {bytes: [0xA6, 0xDD, 0xDF, 0x5C, 0x7E], value: 'ヲン゚¥‾'}, // For Romaji: [0x5C, 0x7E] is '¥‾' + 'IR 166': {bytes: [0xA1, 0xDF, 0xFB], value: 'ก฿๛'}, + // Multi-byte without extensions + 'IR 192': {bytes: [0xF0, 0x9F, 0x87, 0xBA, 0xF0, 0x9F, 0x87, 0xB8, 0xF0, 0x9F, 0x9A, 0x80], value: '🇺🇸🚀'}, + 'GB18030': {bytes: [0x81, 0x30, 0x8A, 0x30, 0xA8, 0xA2, 0xFE, 0x5F], value: 'ãá㥮'}, + 'GBK': {bytes: [0xC3, 0xED, 0xBD, 0x6E], value: '庙絥'}, + // Multi-byte with extensions + 'IR 58': {bytes: [0xB5, 0xDA, 0xD2, 0xBB, 0xD0, 0xD0, 0xCE, 0xC4, 0xD7, 0xD6, 0xA1, 0xA3], value: '第一行文字。'}, + 'IR 87': {bytes: [0x21, 0x38, 0x22, 0x76, 0x30, 0x21, 0x3B, 0x33, 0x45, 0x44], value: '仝♪亜山田'}, + 'IR 149': {bytes: [0xD1, 0xCE, 0xD4, 0xD7], value: '吉洞'}, + 'IR 159': {bytes: [0x32, 0x30, 0x6D, 0x60], value: '傺龡'}, +}; + +function getDelimiterExpectedBytes(escapeSequence, value, delimiters) { + let bytes = []; + for (let delimiter of delimiters.split('')) { + bytes = bytes.concat(escapeSequence.slice(0)); + bytes.push(value); + bytes.push(delimiter.codePointAt(0)); + bytes.push(value); + } + return bytes; +} + +function testSingleCharacterSet(characterSet) { + // Arrange + const example = examples[characterSet.replace('ISO_', '')]; + + // Act + const returnValue = convertBytes(characterSet, new Uint8Array(example.bytes)); + + // Assert + expect(returnValue).to.equal(example.value); +} + +function testCharacterSetExtensions(characterSetStrings) { + // Arrange + let bytes = []; + let expectedValue = ''; + for (let i = 0; i < characterSetStrings.length; i++) { + const characterSet = characterSetStrings[i].replace('ISO_', 'ISO 2022 '); + const exampleName = characterSet.replace('ISO 2022 ', ''); + const elements = characterSets[characterSet].elements; + let escape = []; + if (i > 0) { + for (let sequenceIndex = 0; sequenceIndex < elements.length; sequenceIndex++) { + if (!elements[sequenceIndex].isASCII) + escape = escape.concat(elements[sequenceIndex].escapeSequence); + } + } + bytes = bytes.concat(escape).concat(examples[exampleName].bytes); + expectedValue += examples[exampleName].value; + } + + // Act + const returnValue = convertBytes(characterSetStrings.join('\\'), new Uint8Array(bytes), {vr: 'LT'}); + + // Assert + expect(returnValue).to.equal(expectedValue); +} + +describe('convertBytes', () => { + + describe('single byte without extensions', () => { + + it('should properly convert ISO_IR 6', () => { + testSingleCharacterSet('ISO_IR 6'); + }); + + it('should properly convert ISO_IR 100', () => { + testSingleCharacterSet('ISO_IR 100'); + }); + + it('should properly convert ISO_IR 101', () => { + testSingleCharacterSet('ISO_IR 101'); + }); + + it('should properly convert ISO_IR 109', () => { + testSingleCharacterSet('ISO_IR 109'); + }); + + it('should properly convert ISO_IR 110', () => { + testSingleCharacterSet('ISO_IR 110'); + }); + + it('should properly convert ISO_IR 144', () => { + testSingleCharacterSet('ISO_IR 144'); + }); + + it('should properly convert ISO_IR 127', () => { + testSingleCharacterSet('ISO_IR 127'); + }); + + it('should properly convert ISO_IR 126', () => { + testSingleCharacterSet('ISO_IR 126'); + }); + + it('should properly convert ISO_IR 138', () => { + testSingleCharacterSet('ISO_IR 138'); + }); + + it('should properly convert ISO_IR 148', () => { + testSingleCharacterSet('ISO_IR 148'); + }); + + it('should properly convert ISO_IR 13', () => { + testSingleCharacterSet('ISO_IR 13'); + }); + + it('should properly convert ISO_IR 166', () => { + testSingleCharacterSet('ISO_IR 166'); + }); + + it('should properly convert ISO_IR 192', () => { + testSingleCharacterSet('ISO_IR 192'); + }); + + it('should properly convert GB18030', () => { + testSingleCharacterSet('GB18030'); + }); + + it('should properly convert GBK', () => { + testSingleCharacterSet('GBK'); + }); + + it('should properly convert GBK with promises', () => { + // Arrange + const example = examples['GBK']; + + // Act + return convertBytesPromise('GBK', new Uint8Array(example.bytes)).then(returnValue => { + expect(returnValue).to.equal(example.value); + }); + }); + }); + + describe('single byte with extensions', () => { + it('should properly convert ISO 2022 IR 100', () => { + testCharacterSetExtensions(['ISO 2022 IR 6', 'ISO 2022 IR 100']); + }); + + it('should properly convert from ISO 2022 IR 100', () => { + testCharacterSetExtensions(['ISO 2022 IR 100', 'ISO 2022 IR 6']); + }); + + it('should properly convert to ISO 2022 IR 101', () => { + testCharacterSetExtensions(['ISO 2022 IR 6', 'ISO 2022 IR 101']); + }); + + it('should properly convert to ISO 2022 IR 109', () => { + testCharacterSetExtensions(['ISO 2022 IR 6', 'ISO 2022 IR 109']); + }); + + it('should properly convert to ISO 2022 IR 110', () => { + testCharacterSetExtensions(['ISO 2022 IR 6', 'ISO 2022 IR 110']); + }); + + it('should properly convert to ISO 2022 IR 144', () => { + testCharacterSetExtensions(['ISO 2022 IR 6', 'ISO 2022 IR 144']); + }); + + it('should properly convert to ISO 2022 IR 127', () => { + testCharacterSetExtensions(['ISO 2022 IR 6', 'ISO 2022 IR 127']); + }); + + it('should properly convert to ISO 2022 IR 126', () => { + testCharacterSetExtensions(['ISO 2022 IR 6', 'ISO 2022 IR 126']); + }); + + it('should properly convert to ISO 2022 IR 138', () => { + testCharacterSetExtensions(['ISO 2022 IR 6', 'ISO 2022 IR 138']); + }); + + it('should properly convert to ISO 2022 IR 148', () => { + testCharacterSetExtensions(['ISO 2022 IR 6', 'ISO 2022 IR 148']); + }); + + it('should properly convert ISO 2022 IR 13', () => { // this test ensures both code elements get selected + // Arrange + const bytes = examples['IR 6'].bytes.concat(characterSets['ISO 2022 IR 13'].elements[0].escapeSequence).concat(characterSets['ISO 2022 IR 13'].elements[1].escapeSequence).concat(examples['IR 13'].bytes); + const expectedValue = examples['IR 6'].value + examples['IR 13'].value; + + // Act + const returnValue = convertBytes("ISO 2022 IR 6\\ISO 2022 IR 13", new Uint8Array(bytes), {vr: 'LT'}); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('should properly convert to ISO 2022 IR 166', () => { + testCharacterSetExtensions(['ISO 2022 IR 6', 'ISO 2022 IR 166']); + }); + + it('should properly convert to ISO 2022 IR 87', () => { + testCharacterSetExtensions(['ISO 2022 IR 6', 'ISO 2022 IR 87']); + }); + + it('should properly convert to ISO 2022 IR 159', () => { + testCharacterSetExtensions(['ISO 2022 IR 6', 'ISO 2022 IR 159']); + }); + + it('should properly convert to ISO 2022 IR 149', () => { + testCharacterSetExtensions(['ISO 2022 IR 6', 'ISO 2022 IR 149']); + }); + + it('should properly convert to ISO 2022 IR 58', () => { + testCharacterSetExtensions(['ISO 2022 IR 6', 'ISO 2022 IR 58']); + }); + + it('should properly convert ISO 2022 IR 87 with ISO 2022 IR 149 (separate active code elements)', () => { + // Since these exist in different code elements, they can be activated simultaneously and not need an escape sequence to switch back + // Arrange + const bytes = characterSets['ISO 2022 IR 149'].elements[0].escapeSequence.concat(examples['IR 149'].bytes).concat(examples['IR 87'].bytes); + const expectedValue = examples['IR 149'].value + examples['IR 87'].value; + + // Act + const returnValue = convertBytes("ISO 2022 IR 87\\ISO 2022 IR 149", new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('should properly convert to ISO 2022 IR 13 with promises', () => { + // Arrange + const bytes = examples['IR 6'].bytes.concat(characterSets['ISO 2022 IR 13'].elements[0].escapeSequence).concat(examples['IR 13'].bytes); + const expectedValue = examples['IR 6'].value + examples['IR 13'].value; + + // Act + return convertBytesPromise('ISO 2022 IR 6\\ISO 2022 IR 13', new Uint8Array(bytes), {vr: 'LT'}).then(returnValue => { + expect(returnValue).to.equal(expectedValue); + }); + }); + }); + + describe('robustness', () => { + it('works when ending with an escape sequence', () => { + // Arrange + const bytes = examples['IR 6'].bytes.concat(characterSets['ISO 2022 IR 100'].elements[1].escapeSequence); + const expectedValue = examples['IR 6'].value; + + // Act + const returnValue = convertBytes('ISO 2022 IR 6\\ISO 2022 IR 100', new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('should print ASCII characters with only ISO 2022 IR 149 selected', () => { + // Arrange + const bytes = examples['IR 6'].bytes; + const expectedValue = examples['IR 6'].value; + + // Act + const returnValue = convertBytes('ISO 2022 IR 149', new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('should print characters with only ISO 2022 IR 13 selected', () => { + // Arrange + const bytes = characterSets['ISO 2022 IR 13'].elements[1].escapeSequence.concat(examples['IR 13'].bytes); + const expectedValue = examples['IR 13'].value; + + // Act + const returnValue = convertBytes('ISO 2022 IR 149\\ISO 2022 IR 13', new Uint8Array(bytes), {vr: 'LT'}); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('should print ASCII characters with only ISO 2022 IR 58 selected', () => { + // Arrange + const bytes = examples['IR 6'].bytes; + const expectedValue = examples['IR 6'].value; + + // Act + const returnValue = convertBytes('ISO 2022 IR 58', new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('should ignore any character sets after a multi-byte non-extension character set', () => { + // Arrange + const bytes = examples['IR 192'].bytes.concat([0xC2, 0xA9]); + const expectedValue = examples['IR 192'].value + '©'; + + // Act + const returnValue = convertBytes('ISO_IR 192\\ISO 2022 IR 58', new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('should ignore any multi-byte non-extension character sets that are not first', () => { + // Arrange + const bytes = examples['IR 58'].bytes; + const expectedValue = examples['IR 58'].value; + + // Act + const returnValue = convertBytes('ISO 2022 IR 58\\ISO_IR 192', new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('should convert control characters', () => { + // Arrange + const bytes = [0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0xB, 0xE, 0xF]; + const expectedValue = '\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u000B\u000E\u000F'; + + // Act + const returnValue = convertBytes('ISO 2022 IR 6', new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('should only use the first occurrence of a character set', () => { + testCharacterSetExtensions(['ISO 2022 IR 6', 'ISO 2022 IR 127', 'ISO 2022 IR 6']); + }); + + it('should switch to code extensions when multiple non-extension character sets are found', () => { + testCharacterSetExtensions(['ISO_IR 6', 'ISO_IR 13']); + }); + + it('should allow ISO 2022 IR 87 to be the first character set', () => { + testCharacterSetExtensions(['ISO 2022 IR 87']); + }); + + it('should allow ISO 2022 IR 159 to be the first character set', () => { + testCharacterSetExtensions(['ISO 2022 IR 159']); + }); + + it('should allow ISO 2022 IR 149 to be the first character set', () => { + testCharacterSetExtensions(['ISO 2022 IR 149']); + }); + + it('should allow ISO 2022 IR 58 to be the first character set (defaults to ASCII in G0)', () => { + testCharacterSetExtensions(['ISO 2022 IR 58', 'ISO 2022 IR 166']); + // Arrange + const bytes = examples['IR 6'].bytes.concat(examples['IR 58'].bytes).concat(characterSets['ISO 2022 IR 166'].elements[1].escapeSequence).concat(examples['IR 166'].bytes); + const expectedValue = examples['IR 6'].value + examples['IR 58'].value + examples['IR 166'].value; + + // Act + const returnValue = convertBytes("ISO 2022 IR 58\\ISO 2022 IR 166", new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('LT VR resets to the first character set after control characters', () => { + // Arrange + const bytes = getDelimiterExpectedBytes([0x1B, 0x28, 0x4A], 0x5C, '\r\n\f\t^='); + const expectedValue = '¥\r\\¥\n\\¥\f\\¥\t\\¥^¥¥=¥'; + + // Act + const returnValue = convertBytes('ISO 2022 IR 149\\ISO 2022 IR 13', new Uint8Array(bytes), {vr: 'LT'}); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('PN VR resets to the first character set after delimiters', () => { + // Arrange + const bytes = getDelimiterExpectedBytes([0x1B, 0x2D, 0x54], 0xFB, '\r\n\f\t^=\\'); + const expectedValue = '๛\rû๛\nû๛\fû๛\tû๛^û๛=û๛\\û'; + + // Act + const returnValue = convertBytes('\\ISO 2022 IR 166', new Uint8Array(bytes), {vr: 'PN'}); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('unspecified VR resets to the first character set after delimiters', () => { + // Arrange + const bytes = getDelimiterExpectedBytes([0x1B, 0x2D, 0x54], 0xFB, '\r\n\f\t^=\\'); + const expectedValue = '๛\rû๛\nû๛\fû๛\tû๛^๛๛=๛๛\\û'; // This test verifies that ^= aren't treated as delimeters + + // Act + const returnValue = convertBytes('\\ISO 2022 IR 166', new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('handles empty character set', () => { + // Act + const returnValue = convertBytes('', new Uint8Array(examples['IR 6'].bytes)); + + // Assert + expect(returnValue).to.equal(examples['IR 6'].value); + }); + + it('handles empty bytes for non-extension character sets', () => { + // Act + const returnValue = convertBytes('ISO_IR 166'); + + // Assert + expect(returnValue).to.equal(''); + }); + + it('handles empty bytes for extension character sets', () => { + // Act + const returnValue = convertBytes('ISO 2022 IR 166'); + + // Assert + expect(returnValue).to.equal(''); + }); + }); + + describe('should throw an exception when', () => { + it('an unknown specific character set is given', () => { + expect(() => convertBytes('foo')).to.throw('Invalid specific character set'); + }); + + it('bytes is not a typed array', () => { + expect(() => convertBytes('ISO_IR 6', {})).to.throw('bytes must be a Uint8Array'); + }); + + it('specificCharacterSet is not a string', () => { + expect(() => convertBytes({})).to.throw('specificCharacterSet must be a string'); + }); + + it('unknown escape sequence encountered', () => { + expect(() => convertBytes('ISO 2022 IR 6', new Uint8Array([0x1B, 0x01]))).to.throw('Unknown escape sequence'); + }); + + it('not enough bytes remain to read an escape sequence', () => { + expect(() => convertBytes('ISO 2022 IR 6', new Uint8Array([0x1B, 0x01]))).to.throw('Unknown escape sequence'); + }); + + it('not enough bytes for a single JIS X character exist', () => { + expect(() => convertBytes('ISO 2022 IR 159', new Uint8Array([0x03]))).to.throw('JIS X string'); + }); + }); + + + describe('DICOM standard examples (from PS 3.5)', () => { + it('H.3-1', () => { + // Arrange + const bytes = [0x59, 0x61, 0x6D, 0x61, 0x64, 0x61, 0x5E, 0x54, 0x61, 0x72, 0x6F, 0x75, 0x3D, 0x1B, 0x24, 0x42, 0x3B, 0x33, 0x45, 0x44, 0x1B, 0x28, 0x42, 0x5E, 0x1B, 0x24, 0x42, 0x42, 0x40, 0x4F, 0x3A, 0x1B, 0x28, 0x42, 0x3D, 0x1B, 0x24, 0x42, 0x24, 0x64, 0x24, 0x5E, 0x24, 0x40, 0x1B, 0x28, 0x42, 0x5E, 0x1B, 0x24, 0x42, 0x24, 0x3F, 0x24, 0x6D, 0x24, 0x26, 0x1B, 0x28, 0x42]; + const expectedValue = 'Yamada^Tarou=山田^太郎=やまだ^たろう'; + const characterSet = '\\ISO 2022 IR 87'; + + // Act + const returnValue = convertBytes(characterSet, new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + // Note: The Japanese decoders for JIS X 0201 supported by browsers (shift-jis and euc-jp) both return the katakana + // as half-width characters, but the DICOM standard's example has full width characters. Therefore the IR 13 characters + // in expectedValue don't match the Unicode of the standard for this example, even though they conceptually represent the + // same character, just half width. + it('H.3-2', () => { + // Arrange + const bytes = [0xD4, 0xCF, 0xC0, 0xDE, 0x5E, 0xC0, 0xDB, 0xB3, 0x3D, 0x1B, 0x24, 0x42, 0x3B, 0x33, 0x45, 0x44, 0x1B, 0x28, 0x4A, 0x5E, 0x1B, 0x24, 0x42, 0x42, 0x40, 0x4F, 0x3A, 0x1B, 0x28, 0x4A, 0x3D, 0x1B, 0x24, 0x42, 0x24, 0x64, 0x24, 0x5E, 0x24, 0x40, 0x1B, 0x28, 0x4A, 0x5E, 0x1B, 0x24, 0x42, 0x24, 0x3F, 0x24, 0x6D, 0x24, 0x26, 0x1B, 0x28, 0x4A]; + const expectedValue = 'ヤマダ^タロウ=山田^太郎=やまだ^たろう'; + const characterSet = 'ISO 2022 IR 13\\ISO 2022 IR 87'; + + // Act + const returnValue = convertBytes(characterSet, new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('I.2-1', () => { + // Arrange + const bytes = [0x48, 0x6F, 0x6E, 0x67, 0x5E, 0x47, 0x69, 0x6C, 0x64, 0x6F, 0x6E, 0x67, 0x3D, 0x1B, 0x24, 0x29, 0x43, 0xFB, 0xF3, 0x5E, 0x1B, 0x24, 0x29, 0x43, 0xD1, 0xCE, 0xD4, 0xD7, 0x3D, 0x1B, 0x24, 0x29, 0x43, 0xC8, 0xAB, 0x5E, 0x1B, 0x24, 0x29, 0x43, 0xB1, 0xE6, 0xB5, 0xBF]; + const expectedValue = 'Hong^Gildong=洪^吉洞=홍^길동'; + const characterSet = '\\ISO 2022 IR 149'; + + // Act + const returnValue = convertBytes(characterSet, new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('I.3-1', () => { + // Arrange + const bytes = [0x1B, 0x24, 0x29, 0x43, 0x54, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x20, 0xC7, 0xD1, 0xB1, 0xDB, 0x20, 0x2e, 0xa, 0xa, 0x1B, 0x24, 0x29, 0x43, 0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x20, 0xC7, 0xD1, 0xB1, 0xDB, 0x20, 0x2c, 0x20, 0x74, 0x6f, 0x6f, 0x2e, 0xa, 0xa, 0x54, 0x68, 0x65, 0x20, 0x74, 0x68, 0x69, 0x72, 0x64, 0x20, 0x6c, 0x69, 0x6e, 0x65]; + const expectedValue = `The first line includes 한글 .\n\nThe second line includes 한글 , too.\n\nThe third line`; + + const characterSet = '\\ISO 2022 IR 149'; + + // Act + const returnValue = convertBytes(characterSet, new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('J.1-1', () => { + // Arrange + const bytes = [0x57, 0x61, 0x6e, 0x67, 0x5e, 0x58, 0x69, 0x61, 0x6f, 0x44, 0x6f, 0x6e, 0x67, 0x3d, 0xe7, 0x8e, 0x8b, 0x5e, 0xe5, 0xb0, 0x8f, 0xe6, 0x9d, 0xb1, 0x3d]; + const expectedValue = 'Wang^XiaoDong=王^小東='; + + const characterSet = 'ISO_IR 192'; + + // Act + const returnValue = convertBytes(characterSet, new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('J.2-1', () => { + // Arrange + const bytes = [0x54, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x20, 0xe4, 0xb8, 0xad, 0xe6, 0x96, 0x87, 0x2e, 0x0d, 0x0a, 0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x20, 0xe4, 0xb8, 0xad, 0xe6, 0x96, 0x87, 0x2c, 0x20, 0x74, 0x6f, 0x6f, 0x2e, 0x0d, 0x0a, 0x54, 0x68, 0x65, 0x20, 0x74, 0x68, 0x69, 0x72, 0x64, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x2e, 0x0d, 0x0a]; + const expectedValue = `The first line includes 中文.\r\nThe second line includes 中文, too.\r\nThe third line.\r\n`; + + const characterSet = 'ISO_IR 192'; + + // Act + const returnValue = convertBytes(characterSet, new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('J.3-1', () => { + // Arrange + const bytes = [0x57, 0x61, 0x6e, 0x67, 0x5e, 0x58, 0x69, 0x61, 0x6f, 0x44, 0x6f, 0x6e, 0x67, 0x3d, 0xcd, 0xf5, 0x5e, 0xd0, 0xa1, 0xb6, 0xab, 0x3d]; + const expectedValue = `Wang^XiaoDong=王^小东=`; + + const characterSet = 'GB18030'; + + // Act + const returnValue = convertBytes(characterSet, new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('J.4-1', () => { + // Arrange + const bytes = [0x54, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x20, 0xd6, 0xd0, 0xce, 0xc4, 0x2e, 0x0d, 0x0a, 0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x20, 0xd6, 0xd0, 0xce, 0xc4, 0x2c, 0x20, 0x74, 0x6f, 0x6f, 0x2e, 0x0d, 0x0a, 0x54, 0x68, 0x65, 0x20, 0x74, 0x68, 0x69, 0x72, 0x64, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x2e, 0x0d, 0x0a]; + const expectedValue = `The first line includes 中文.\r\nThe second line includes 中文, too.\r\nThe third line.\r\n`; + + const characterSet = 'GB18030'; + + // Act + const returnValue = convertBytes(characterSet, new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('K.2-1', () => { + // Arrange + const bytes = [0x5A, 0x68, 0x61, 0x6E, 0x67, 0x5E, 0x58, 0x69, 0x61, 0x6F, 0x44, 0x6F, 0x6E, 0x67, 0x3D, 0x1B, 0x24, 0x29, 0x41, 0xD5, 0xC5, 0x5E, 0x1B, 0x24, 0x29, 0x41, 0xD0, 0xA1, 0xB6, 0xAB, 0x3D, 0x20]; + const expectedValue = `Zhang^XiaoDong=张^小东= `; + + const characterSet = '\\ISO 2022 IR 58'; + + // Act + const returnValue = convertBytes(characterSet, new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + + it('K.3-1', () => { + // Arrange + const bytes = [0x31, 0x29, 0x20, 0x1B, 0x24, 0x29, 0x41, 0xB5, 0xDA, 0xD2, 0xBB, 0xD0, 0xD0, 0xCE, 0xC4, 0xD7, 0xD6, 0xA1, 0xA3, 0x0D, 0x0A, 0x32, 0x29, 0x20, 0x1B, 0x24, 0x29, 0x41, 0xB5, 0xDA, 0xB6, 0xFE, 0xD0, 0xD0, 0xCE, 0xC4, 0xD7, 0xD6, 0xA1, 0xA3, 0x0D, 0x0A, 0x33, 0x29, 0x20, 0x1B, 0x24, 0x29, 0x41, 0xB5, 0xDA, 0xC8, 0xFD, 0xD0, 0xD0, 0xCE, 0xC4, 0xD7, 0xD6, 0xA1, 0xA3, 0x0D, 0x0A]; + const expectedValue = `1) 第一行文字。\r\n2) 第二行文字。\r\n3) 第三行文字。\r\n`; + + const characterSet = '\\ISO 2022 IR 58'; + + // Act + const returnValue = convertBytes(characterSet, new Uint8Array(bytes)); + + // Assert + expect(returnValue).to.equal(expectedValue); + }); + }); + + it('all extensions', () => { + const characterSets = ['ISO 2022 IR 6','ISO 2022 IR 100','ISO 2022 IR 101','ISO 2022 IR 109','ISO 2022 IR 110','ISO 2022 IR 144','ISO 2022 IR 127','ISO 2022 IR 126','ISO 2022 IR 138','ISO 2022 IR 148','ISO 2022 IR 13','ISO 2022 IR 166','ISO 2022 IR 58','ISO 2022 IR 87','ISO 2022 IR 149','ISO 2022 IR 159']; + testCharacterSetExtensions(characterSets); + }); + + describe('exceptions', () => { + it('should throw when ', () => { + testCharacterSetExtensions(['ISO 2022 IR 6', 'ISO 2022 IR 100']); + }); + }); + +});