Skip to content

Commit e8bc81c

Browse files
authored
Merge pull request #101 from ourzora/BACK-2749
BACK-2749: support svg image uri in opensea parser
2 parents 564b6c4 + 87faddd commit e8bc81c

File tree

9 files changed

+65
-9
lines changed

9 files changed

+65
-9
lines changed

docs/changelog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## v0.3.3
4+
5+
- Fix an issue in `OpenseaParser` where the plain-text svg wouldn't be recognized as valid image uri
6+
- Add check in `DefaultCatchallParser` to require that `raw_data` be a `dict`
7+
38
## v0.3.2
49

510
- Fix an issue in `DataURIAdapter` where plain-text json data uri would get ignored

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Getting Started
22

3-
Documentation for version: **v0.3.2**
3+
Documentation for version: **v0.3.3**
44

55
## Overview
66

offchain/metadata/parsers/catchall/default_catchall.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,4 +242,7 @@ def should_parse_token( # type: ignore[no-untyped-def]
242242
Returns:
243243
bool: whether or not the collection parser handles this token.
244244
"""
245-
return bool(token.uri and raw_data)
245+
246+
if raw_data is not None and not isinstance(raw_data, dict):
247+
logger.info("DefaultCatchallParser skips token {token} due to invalid raw data")
248+
return bool(token.uri and raw_data is not None and isinstance(raw_data, dict))

offchain/metadata/parsers/collection/artblocks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def get_additional_fields(self, raw_data: dict) -> list[MetadataField]: # type:
224224

225225
return additional_fields
226226

227-
def parse_traits(self, raw_data: dict) -> Optional[list[Attribute]]: # type: ignore[type-arg] # noqa: E501
227+
def parse_traits(self, raw_data: dict) -> list[Attribute]: # type: ignore[type-arg] # noqa: E501
228228
traits = raw_data.get("traits")
229229
if not traits or not isinstance(traits, list):
230230
return # type: ignore[return-value]

offchain/metadata/parsers/collection/ens.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def get_additional_fields(self, raw_data: dict) -> list[MetadataField]: # type:
5858
)
5959
return additional_fields
6060

61-
def parse_attributes(self, raw_data: dict) -> Optional[list[Attribute]]: # type: ignore[type-arg] # noqa: E501
61+
def parse_attributes(self, raw_data: dict) -> list[Attribute]: # type: ignore[type-arg] # noqa: E501
6262
attributes = raw_data.get("attributes")
6363
if not attributes or not isinstance(attributes, list):
6464
return # type: ignore[return-value]

offchain/metadata/parsers/schema/opensea.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import base64
12
from typing import Optional
23

34
from offchain.metadata.models.metadata import (
@@ -90,6 +91,9 @@ def parse_metadata(self, token: Token, raw_data: dict, *args, **kwargs) -> Optio
9091
image = None
9192
image_uri = raw_data.get("image") or raw_data.get("image_data")
9293
if image_uri:
94+
if image_uri.startswith("<svg"):
95+
image_uri_encoded = base64.b64encode(image_uri.encode("utf-8")).decode("utf-8")
96+
image_uri = f"data:image/svg+xml;base64,{image_uri_encoded}"
9397
image_mime, image_size = self.fetcher.fetch_mime_type_and_size(image_uri)
9498
image = MediaDetails(size=image_size, uri=image_uri, mime_type=image_mime)
9599

@@ -140,6 +144,9 @@ async def _gen_parse_metadata_impl(self, token: Token, raw_data: dict, *args, **
140144
image = None
141145
image_uri = raw_data.get("image") or raw_data.get("image_data")
142146
if image_uri:
147+
if image_uri.startswith("<svg"):
148+
image_uri_encoded = base64.b64encode(image_uri.encode("utf-8")).decode("utf-8")
149+
image_uri = f"data:image/svg+xml;base64,{image_uri_encoded}"
143150
image_mime, image_size = await self.fetcher.gen_fetch_mime_type_and_size(
144151
image_uri
145152
)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "offchain"
3-
version = "0.3.2"
3+
version = "0.3.3"
44
description = "Open source metadata processing framework"
55
authors = ["Zora eng <eng@zora.co>"]
66
readme = "README.md"

tests/metadata/parsers/test_default_catchall_parser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66

77
from offchain.metadata.fetchers.metadata_fetcher import MetadataFetcher
8-
from offchain.metadata.models.metadata import Metadata, MetadataStandard
8+
from offchain.metadata.models.metadata import Metadata
99
from offchain.metadata.models.token import Token
1010
from offchain.metadata.parsers.catchall.default_catchall import DefaultCatchallParser
1111

@@ -14,7 +14,7 @@ class TestDefaultCatchallParser:
1414
token = Token(
1515
chain_identifier="ETHEREUM-MAINNET",
1616
collection_address="0x74cb086a1611cc9ca672f458b7742dd4159ac9db",
17-
token_id="80071",
17+
token_id=80071,
1818
uri="https://api.dego.finance/gego-token-v2/80071",
1919
)
2020
raw_data = {

tests/metadata/parsers/test_opensea_parser.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from unittest.mock import MagicMock
44

55
import pytest
6+
import base64
67

78
from offchain.metadata.adapters.ipfs import IPFSAdapter
89
from offchain.metadata.fetchers.metadata_fetcher import MetadataFetcher
@@ -12,7 +13,6 @@
1213
Metadata,
1314
MetadataField,
1415
MetadataFieldType,
15-
MetadataStandard,
1616
)
1717
from offchain.metadata.models.token import Token
1818
from offchain.metadata.parsers.schema.opensea import OpenseaParser
@@ -22,7 +22,7 @@ class TestOpenseaParser:
2222
token = Token(
2323
chain_identifier="ETHEREUM-MAINNET",
2424
collection_address="0x5180db8f5c931aae63c74266b211f580155ecac8",
25-
token_id="1",
25+
token_id=1,
2626
uri="ipfs://QmSr3vdMuP2fSxWD7S26KzzBWcAN1eNhm4hk1qaR3x3vmj/1.json",
2727
)
2828

@@ -229,3 +229,44 @@ async def test_opensea_parser_gen_parses_metadata(self, raw_crypto_coven_metadat
229229
token=self.token, raw_data=raw_crypto_coven_metadata
230230
)
231231
assert metadata
232+
233+
@pytest.mark.asyncio
234+
async def test_opensea_parser_parses_token_with_xml_image(self):
235+
parser = OpenseaParser(fetcher=MetadataFetcher()) # type: ignore[abstract]
236+
token = Token(
237+
chain_identifier="BASE-MAINNET",
238+
collection_address="0x00000000001594c61dd8a6804da9ab58ed2483ce",
239+
token_id=91107139416293979998100172630436458595092238971,
240+
uri="https://metadata.nfts2me.com/api/ownerTokenURI/8453/91107139416293979998100172630436458595092238971/574759207385280074438303243253258373278259074888/10000/",
241+
)
242+
raw_data = {
243+
"name": "NFTs2Me Collection Owner - drako",
244+
"description": "Represents **Ownership of the NFTs2Me Collection** with address '[0x0fF562Ab42325222cF72971d32ED9CDF373b927B](https://0x0fF562Ab42325222cF72971d32ED9CDF373b927B_8453.nfts2.me/)'.\n\nTransferring this NFT implies changing the owner of the collection, as well as who will receive 100% of the profits from primary and secondary sales.\n\n[NFTs2Me](https://nfts2me.com/) is a showcase of unique digital creations from talented creators who have used the tool to generate their own NFT projects. These projects range from digital art and collectibles to gaming items and more, all with the added value of being verified on the blockchain. With a wide range of styles and themes, the [NFTs2Me](https://nfts2me.com/) tool offers something for every fan of the growing NFT space.",
245+
"image_data": '<svg viewBox="0 0 499.99998 499.99998" width="500" height="500" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="background: white;"><style type="text/css">.st0{fill:#6CB7E6;}.st1{fill:#147ABF;}</style><g transform="matrix(2.002645,0,0,2.002645,40.953902,-319.12168)"><circle class="st0" cx="105.05" cy="204.58" r="8.1800003"/><path class="st0" d="M 80.05,327.28 54.21,312.9 V 253.94 L 86.54,236.99 V 356.55 L 36.4,325.86 c 0,0 -3.28,-1.71 -3.28,-6.2 0,-4.49 0,-71.78 0,-71.78 0,0 -0.57,-4.27 3.85,-6.76 4.42,-2.49 60.67,-35.39 60.67,-35.39 l 2.07,5.04 c 0,0 -53.67,31.08 -57.95,33.85 -1.78,1.15 -3.15,2.11 -3.15,4.98 0,2.98 -0.01,54.64 -0.01,67.42 0,2.51 0.22,3.4 2.27,5.19 1.9,1.64 39.16,24.28 39.16,24.28 z"/><circle class="st0" cx="103.73" cy="363.79001" r="8.1800003"/><path class="st0" d="m 128.72,241.09 25.85,14.38 v 58.96 l -32.33,16.95 V 211.82 l 50.13,30.69 c 0,0 3.28,1.71 3.28,6.2 0,4.49 0,71.78 0,71.78 0,0 0.57,4.27 -3.85,6.76 -4.41,2.49 -60.67,35.39 -60.67,35.39 l -2.07,-5.04 c 0,0 53.67,-31.08 57.95,-33.85 1.78,-1.15 3.15,-2.11 3.15,-4.98 0,-2.98 0.01,-54.64 0.01,-67.42 0,-2.51 -0.22,-3.4 -2.27,-5.19 -1.9,-1.64 -39.16,-24.28 -39.16,-24.28 z"/></g> <g transform="matrix(2.002645,0,0,2.002645,-601.56128,-329.35424)"><polygon class="st1" points="122.24,331.38 86.53,291.04 86.53,236.99 122.24,277.39" transform="translate(320.83329,5.1095282)"/></g> <path d="m 113.85595,58.83376 h 272.2881 a 36.305083,22.490145 0 0 1 36.30509,22.490149 V 418.67609 a 36.305083,22.490145 0 0 1 -36.30509,22.49015 H 113.85595 A 36.305083,22.490145 0 0 1 77.55086,418.67609 V 81.323909 A 36.305083,22.490145 0 0 1 113.85595,58.83376 Z" style="fill:none;stroke:#147ABF;stroke-width:3;stroke-opacity:1"/> <path id="text-path" d="m 109.75187,53.071033 h 280.49626 a 37.399504,23.168113 0 0 1 37.39951,23.168117 v 347.5217 a 37.399504,23.168113 0 0 1 -37.39951,23.16812 H 109.75187 A 37.399504,23.168113 0 0 1 72.35236,423.76085 V 76.23915 a 37.399504,23.168113 0 0 1 37.39951,-23.168117 z" style="fill:none;"/><text text-rendering="optimizeSpeed"><textPath startOffset="-100%" fill="black" font-family="\'Courier New\', monospace" font-size="16px" xlink:href="#text-path">COLLECTION • drako <animate additive="sum" attributeName="startOffset" from="0%" to="100%" begin="0s" dur="30s" repeatCount="indefinite"/> </textPath> <textPath startOffset="0%" fill="black" font-family="\'Courier New\', monospace" font-size="16px" xlink:href="#text-path">COLLECTION • drako <animate additive="sum" attributeName="startOffset" from="0%" to="100%" begin="0s" dur="30s" repeatCount="indefinite"/> </textPath> <textPath startOffset="50%" fill="black" font-family="\'Courier New\', monospace" font-size="16px" xlink:href="#text-path">OWNER • 0x64Ad181f69466bD4D15076aC1d33a22c6Cc9d748 <animate additive="sum" attributeName="startOffset" from="0%" to="100%" begin="0s" dur="30s" repeatCount="indefinite"/> </textPath> <textPath startOffset="-50%" fill="black" font-family="\'Courier New\', monospace" font-size="16px" xlink:href="#text-path">OWNER • 0x64Ad181f69466bD4D15076aC1d33a22c6Cc9d748 <animate additive="sum" attributeName="startOffset" from="0%" to="100%" begin="0s" dur="30s" repeatCount="indefinite"/> </textPath> </text></svg>',
246+
"background_color": "#FFFFFF",
247+
"attributes": [
248+
{"trait_type": "Collection Name", "value": "drako"},
249+
{
250+
"trait_type": "Collection Address",
251+
"value": "0x0fF562Ab42325222cF72971d32ED9CDF373b927B",
252+
},
253+
{
254+
"trait_type": "Owner Address",
255+
"value": "0x64Ad181f69466bD4D15076aC1d33a22c6Cc9d748",
256+
},
257+
{"display_type": "number", "trait_type": "Revenue", "value": 100},
258+
],
259+
"external_url": "https://0x0fF562Ab42325222cF72971d32ED9CDF373b927B_8453.nfts2.me/",
260+
}
261+
metadata = await parser._gen_parse_metadata_impl(token=token, raw_data=raw_data)
262+
svg_encoded = base64.b64encode(
263+
raw_data.get("image_data").encode("utf-8")
264+
).decode("utf-8")
265+
expected_image_uri = f"data:image/svg+xml;base64,{svg_encoded}"
266+
assert metadata
267+
assert metadata.image == MediaDetails(
268+
size=3256,
269+
sha256=None,
270+
uri=expected_image_uri,
271+
mime_type="image/svg+xml",
272+
)

0 commit comments

Comments
 (0)