diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 815d21c..783d11f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,12 +9,12 @@ jobs: strategy: matrix: - node-version: [12.x] + node-version: [22.x] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: npm install, build, and test @@ -24,7 +24,7 @@ jobs: npm test env: CI: true - - uses: actions/upload-artifact@v1 + - uses: actions/upload-artifact@v4 with: name: dezoomify.zip path: ./dezoomify.zip diff --git a/background.js b/background.js index ff92fc1..fc7bb82 100644 --- a/background.js +++ b/background.js @@ -1,36 +1,3 @@ -const DEZOOMIFY_URL = "https://dezoomify.ophir.dev/#"; - -const iiifpath = new RegExp( // IIIF API image URL - "/\\^?(full|square|(pct:)?\\d+,\\d+,\\d+,\\d+)" + // region - "/(full|max|\\d+,|,\\d+|pct:\\d+|!?\\d+,\\d+)" + // size - "/!?[1-3]?[0-9]?[0-9]" + // rotation - "/(color|gray|bitonal|default|native)" + // quality - "\\.(jpe?g|tiff?|png|gif|jp2|pdf|webp)" // format -); - -const META_REGEX = new RegExp([ - /\/ImageProperties.xml/, // Zoomify - /\/info.json/, // IIIF - /\?FIF=/, // IIPImage - /_files\/0\/0_0.jpg(?:\?.*)?$/, // OpenSeadragon - /\.img.\\?cmd=info/, - /getTilesInfo\?object_id/, - /\.pff(&requestType=1)?$/, // Zoomify PFF - /\.ecw(?:\?.*)?$/, // Hungaricana - /\/p.xml(?:\?.*)?$/, // Mnesys - iiifpath, - /artsandculture\.google\.com\/asset\// // Google Arts -].map(e => e.source).join('|')); -const META_REPLACE = [ - { pattern: /\.dzi(?:\?.*)?$/, replacement: '_files/0/0_0.jpg' }, - { pattern: /_files\/\d+\/\d+_\d+\.jpg(?:\?.*)?$/, replacement: '_files/0/0_0.jpg' }, - { pattern: /\/TileGroup\d+\/\d+-\d+-\d+.jpg(?:\?.*)?$/, replacement: '/ImageProperties.xml' }, - { pattern: /\/ImageProperties\.xml\?t\w+$/, replacement: '/ImageProperties.xml' }, - { pattern: /(\?FIF=[^&]*)&.*/, replacement: '$1' }, // IIPImage - { pattern: /(http.*artsandculture\.google\.com\/asset\/.+\/.+)\?.*/, replacement: '$1' }, - { pattern: iiifpath, replacement: '/info.json' }, - { pattern: /getTilesInfo\?object_id=(.*)&callback.*/, replacement: 'getTilesInfo?object_id=$1' }, -]; // @ts-ignore const VALID_RESOURCE_TYPES = new Set(Object.values(chrome.webRequest['ResourceType'])); @@ -148,12 +115,8 @@ class PageListener { * @param {{url:string }} request */ handleRequest({ url }) { - for (const { pattern, replacement } of META_REPLACE) { - url = url.replace(pattern, replacement); - } - if (META_REGEX.test(url) && !url.startsWith(DEZOOMIFY_URL)) { - this.foundZoomableImage(url); - } + const recognizedUrl = recognizeZoomableUrl(url); + if (recognizedUrl) this.foundZoomableImage(recognizedUrl); } /** diff --git a/build.sh b/build.sh index 6a06254..a43e3fa 100755 --- a/build.sh +++ b/build.sh @@ -4,5 +4,5 @@ TMPDIR=$(mktemp -d) jq '.version |= '"$version"' ' < manifest.json > "$TMPDIR/manifest.json" rm -f dezoomify.zip -zip -r dezoomify.zip icons/ background.js manifest.json -zip -r --junk-paths dezoomify.zip "$TMPDIR/manifest.json" \ No newline at end of file +zip -r dezoomify.zip icons/ url-recognition.js background.js manifest.json +zip -r --junk-paths dezoomify.zip "$TMPDIR/manifest.json" diff --git a/manifest.json b/manifest.json index f02d709..66341c3 100644 --- a/manifest.json +++ b/manifest.json @@ -27,7 +27,8 @@ ], "background": { "scripts": [ + "url-recognition.js", "background.js" ] } -} \ No newline at end of file +} diff --git a/package.json b/package.json index e472bfc..30cc3df 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Detect zoomable images in web pages and download them with dezoomify", "main": "background.js", "scripts": { - "test": "tsc", + "test": "node test/url-recognition.test.js && tsc", "build": "bash -x build.sh" }, "repository": { diff --git a/test/url-recognition.test.js b/test/url-recognition.test.js new file mode 100644 index 0000000..791524a --- /dev/null +++ b/test/url-recognition.test.js @@ -0,0 +1,37 @@ +const assert = require("assert"); +const fs = require("fs"); +const vm = require("vm"); + +const source = fs.readFileSync("url-recognition.js", "utf8"); +const context = {}; +vm.runInNewContext(source, context); +const recognize = context.recognizeZoomableUrl; + +const cases = [ + ["https://example.com/ImageProperties.xml?t123", "https://example.com/ImageProperties.xml"], + ["https://example.com/image/info.json", "https://example.com/image/info.json"], + ["https://example.com/image.dzi?cache=1", "https://example.com/image_files/0/0_0.jpg"], + ["https://example.com/image_files/12/3_4.jpg?cache=1", "https://example.com/image_files/0/0_0.jpg"], + ["https://example.com/image_files/12/3_4.jpeg", "https://example.com/image_files/0/0_0.jpg"], + ["https://example.com/TileGroup2/5-3-4.jpg", "https://example.com/ImageProperties.xml"], + ["https://example.com/iip?FIF=image.tif&WID=500", "https://example.com/iip?FIF=image.tif"], + ["https://example.com/image.imgi?cmd=info", "https://example.com/image.imgi?cmd=info"], + ["https://example.com/image.pff", "https://example.com/image.pff"], + ["https://example.com/image.ecw", "https://example.com/image.ecw"], + ["https://example.com/viewer/p.xml", "https://example.com/viewer/p.xml"], + ["https://example.com/id/full/512,/0/default.jpg", "https://example.com/id/info.json"], + ["https://www.rijksmuseum.nl/api/getTilesInfo?object_id=1&callback=x", "https://www.rijksmuseum.nl/api/getTilesInfo?object_id=1"], + ["https://artsandculture.google.com/asset/title/id?hl=en", "https://artsandculture.google.com/asset/title/id"], +]; + +for (const [input, expected] of cases) { + assert.strictEqual(recognize(input), expected, input); +} + +assert.strictEqual(recognize("https://example.com/photo.jpg"), undefined); +assert.strictEqual( + recognize("https://dezoomify.ophir.dev/#https://example.com/info.json"), + undefined +); + +console.log(`Passed ${cases.length + 2} URL recognition tests`); diff --git a/tsconfig.json b/tsconfig.json index 704d8b1..acb2e71 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,9 +14,10 @@ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ }, "files": [ + "url-recognition.js", "background.js" ], "types": [ "chrome" ] -} \ No newline at end of file +} diff --git a/url-recognition.js b/url-recognition.js new file mode 100644 index 0000000..c1ff27d --- /dev/null +++ b/url-recognition.js @@ -0,0 +1,47 @@ +const DEZOOMIFY_URL = "https://dezoomify.ophir.dev/#"; + +const iiifpath = new RegExp( + "/\\^?(full|square|(pct:)?\\d+,\\d+,\\d+,\\d+)" + + "/(full|max|\\d+,|,\\d+|pct:\\d+|!?\\d+,\\d+)" + + "/!?[1-3]?[0-9]?[0-9]" + + "/(color|gray|bitonal|default|native)" + + "\\.(jpe?g|tiff?|png|gif|jp2|pdf|webp)" +); + +const META_REGEX = new RegExp([ + /\/ImageProperties.xml/, + /\/info.json/, + /\?FIF=/, + /_files\/0\/0_0\.jpe?g(?:\?.*)?$/, + /\.img.\?cmd=info/, + /getTilesInfo\?object_id/, + /\.pff(&requestType=1)?$/, + /\.ecw(?:\?.*)?$/, + /\/p.xml(?:\?.*)?$/, + iiifpath, + /artsandculture\.google\.com\/asset\// +].map(e => e.source).join('|')); + +const META_REPLACE = [ + { pattern: /\.dzi(?:\?.*)?$/, replacement: '_files/0/0_0.jpg' }, + { pattern: /_files\/\d+\/\d+_\d+\.jpe?g(?:\?.*)?$/, replacement: '_files/0/0_0.jpg' }, + { pattern: /\/TileGroup\d+\/\d+-\d+-\d+.jpg(?:\?.*)?$/, replacement: '/ImageProperties.xml' }, + { pattern: /\/ImageProperties\.xml\?t\w+$/, replacement: '/ImageProperties.xml' }, + { pattern: /(\?FIF=[^&]*)&.*/, replacement: '$1' }, + { pattern: /(http.*artsandculture\.google\.com\/asset\/.+\/.+)\?.*/, replacement: '$1' }, + { pattern: iiifpath, replacement: '/info.json' }, + { pattern: /getTilesInfo\?object_id=(.*)&callback.*/, replacement: 'getTilesInfo?object_id=$1' }, +]; + +/** + * Return a canonical metadata URL when a request belongs to a supported viewer. + * @param {string} requestUrl + * @returns {string | undefined} + */ +function recognizeZoomableUrl(requestUrl) { + let url = requestUrl; + for (const { pattern, replacement } of META_REPLACE) { + url = url.replace(pattern, replacement); + } + if (META_REGEX.test(url) && !url.startsWith(DEZOOMIFY_URL)) return url; +}