feat: add image plugin for jpeg, png and gif comments and tag svg files
This commit is contained in:
parent
0a19339b98
commit
712fe264be
10 changed files with 341 additions and 43 deletions
|
|
@ -90,3 +90,15 @@ def test_marker_inside_code_is_ignored(cwd):
|
|||
(cwd / 'a.py').write_text('x = "otag: #nope"\n')
|
||||
OrvaniTag().scan('.', False)
|
||||
assert not (cwd / orvani.CONTEXT_FILE).exists()
|
||||
|
||||
|
||||
def test_svg_keeps_xml_declaration_first(cwd):
|
||||
write('a.svg', '<?xml version="1.0"?>\n<svg xmlns="http://www.w3.org/2000/svg"/>\n', '#icon')
|
||||
assert (cwd / 'a.svg').read_text() == '<?xml version="1.0"?>\n<!-- otag: #icon -->\n\n<svg xmlns="http://www.w3.org/2000/svg"/>\n'
|
||||
assert read_back(cwd, 'a.svg') == 'a.svg --- #icon\n'
|
||||
|
||||
|
||||
def test_double_dash_is_refused_in_markup_comments(cwd):
|
||||
with pytest.raises(UnsupportedFile):
|
||||
write('a.svg', '<svg/>\n', '#icon', 'before -- after')
|
||||
assert (cwd / 'a.svg').read_text() == '<svg/>\n'
|
||||
|
|
|
|||
132
tests/test_image.py
Normal file
132
tests/test_image.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import struct
|
||||
import zlib
|
||||
|
||||
import pytest
|
||||
|
||||
import orvanitag.orvani as orvani
|
||||
from orvanitag.entry import Entry
|
||||
from orvanitag.orvani import OrvaniTag
|
||||
from orvanitag.errors import UnsupportedFile
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cwd(tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
return tmp_path
|
||||
|
||||
|
||||
def roundtrip(cwd, name, *tags):
|
||||
"""Writes the tags and reads them back into a fresh entry"""
|
||||
OrvaniTag().add(name, *tags)
|
||||
OrvaniTag().write_metadata(name, 'image')
|
||||
expected = (cwd / orvani.CONTEXT_FILE).read_text()
|
||||
(cwd / orvani.CONTEXT_FILE).unlink()
|
||||
OrvaniTag().read_metadata(name)
|
||||
assert (cwd / orvani.CONTEXT_FILE).read_text() == expected
|
||||
|
||||
|
||||
def clear(name):
|
||||
OrvaniTag().write_metadata(name, 'image', clear=True)
|
||||
|
||||
|
||||
def segment(marker, data):
|
||||
return bytes([0xFF, marker]) + (len(data) + 2).to_bytes(2, 'big') + data
|
||||
|
||||
|
||||
SOI = b'\xff\xd8'
|
||||
APPS = segment(0xE0, b'JFIF\0\1\1\0\0\1\0\1\0\0') + segment(0xE1, b'Exif\0\0')
|
||||
REST = segment(0xDB, bytes(65)) + segment(0xDA, bytes(10)) + b'\x12\x34\xff\xd9'
|
||||
|
||||
|
||||
def test_jpeg_comment_goes_after_the_app_segments(cwd):
|
||||
(cwd / 'a.jpg').write_bytes(SOI + APPS + REST)
|
||||
roundtrip(cwd, 'a.jpg', '+trip', '#beach', 'sunset')
|
||||
assert (cwd / 'a.jpg').read_bytes() == SOI + APPS + segment(0xFE, b'otag: +trip #beach sunset') + REST
|
||||
clear('a.jpg')
|
||||
assert (cwd / 'a.jpg').read_bytes() == SOI + APPS + REST
|
||||
|
||||
|
||||
def test_jpeg_keeps_other_comments(cwd):
|
||||
foreign = segment(0xFE, b'CREATOR: gd-jpeg v1.0')
|
||||
(cwd / 'a.JPEG').write_bytes(SOI + APPS + foreign + REST)
|
||||
assert OrvaniTag().read_metadata('a.JPEG') == Entry()
|
||||
roundtrip(cwd, 'a.JPEG', '#beach')
|
||||
OrvaniTag().add('a.JPEG', '#more')
|
||||
OrvaniTag().write_metadata('a.JPEG', 'image')
|
||||
assert (cwd / 'a.JPEG').read_bytes() == SOI + APPS + segment(0xFE, b'otag: #beach #more') + foreign + REST
|
||||
clear('a.JPEG')
|
||||
assert (cwd / 'a.JPEG').read_bytes() == SOI + APPS + foreign + REST
|
||||
|
||||
|
||||
def test_jpeg_comment_length_is_limited(cwd):
|
||||
(cwd / 'a.jpg').write_bytes(SOI + APPS + REST)
|
||||
OrvaniTag().add('a.jpg', 'x' * 0x10000)
|
||||
with pytest.raises(UnsupportedFile, match='too long'):
|
||||
OrvaniTag().write_metadata('a.jpg', 'image')
|
||||
assert (cwd / 'a.jpg').read_bytes() == SOI + APPS + REST
|
||||
|
||||
|
||||
def chunk(kind, data):
|
||||
return len(data).to_bytes(4, 'big') + kind + data + zlib.crc32(kind + data).to_bytes(4, 'big')
|
||||
|
||||
|
||||
def chunks(data):
|
||||
"""The chunks of a PNG, checking their CRCs"""
|
||||
found, pos = [], 8
|
||||
while pos < len(data):
|
||||
size, kind = int.from_bytes(data[pos:pos + 4], 'big'), data[pos + 4:pos + 8]
|
||||
body = data[pos + 8:pos + 8 + size]
|
||||
assert data[pos + 8 + size:pos + 12 + size] == zlib.crc32(kind + body).to_bytes(4, 'big')
|
||||
found.append((kind, body))
|
||||
pos += 12 + size
|
||||
return found
|
||||
|
||||
|
||||
PNG = b'\x89PNG\r\n\x1a\n'
|
||||
IHDR = chunk(b'IHDR', struct.pack('>IIBBBBB', 1, 1, 8, 0, 0, 0, 0))
|
||||
IMAGE = chunk(b'IDAT', zlib.compress(b'\0\0')) + chunk(b'IEND', b'')
|
||||
|
||||
|
||||
def test_png_comment_chunk_and_others_kept(cwd):
|
||||
foreign = chunk(b'iTXt', b'Comment\0\0\0\0\0Created with GIMP') + chunk(b'tEXt', b'Software\0x')
|
||||
(cwd / 'a.png').write_bytes(PNG + IHDR + foreign + IMAGE)
|
||||
assert OrvaniTag().read_metadata('a.png') == Entry()
|
||||
|
||||
roundtrip(cwd, 'a.png', '#фото', 'a note')
|
||||
data = (cwd / 'a.png').read_bytes()
|
||||
assert chunks(data)[1] == (b'iTXt', b'Comment\0\0\0\0\0' + 'otag: #фото a note'.encode())
|
||||
assert data == PNG + IHDR + chunk(b'iTXt', b'Comment\0\0\0\0\0' + 'otag: #фото a note'.encode()) + foreign + IMAGE
|
||||
|
||||
clear('a.png')
|
||||
assert (cwd / 'a.png').read_bytes() == PNG + IHDR + foreign + IMAGE
|
||||
|
||||
|
||||
GIF_SCREEN = b'\x01\x00\x01\x00\x80\x00\x00' + b'\xff\xff\xff\x00\x00\x00'
|
||||
GIF_IMAGE = b',\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00;'
|
||||
|
||||
|
||||
def test_gif_comment_in_sub_blocks(cwd):
|
||||
foreign = b'\x21\xfe\x07made by\x00'
|
||||
(cwd / 'a.gif').write_bytes(b'GIF89a' + GIF_SCREEN + foreign + GIF_IMAGE)
|
||||
note = 'n' * 300
|
||||
roundtrip(cwd, 'a.gif', '#anim', note)
|
||||
line = f'otag: #anim {note}'.encode()
|
||||
assert (cwd / 'a.gif').read_bytes() == (b'GIF89a' + GIF_SCREEN + b'\x21\xfe\xff' + line[:255]
|
||||
+ bytes([len(line) - 255]) + line[255:] + b'\0' + foreign + GIF_IMAGE)
|
||||
clear('a.gif')
|
||||
assert (cwd / 'a.gif').read_bytes() == b'GIF89a' + GIF_SCREEN + foreign + GIF_IMAGE
|
||||
|
||||
|
||||
def test_gif87a_becomes_89a(cwd):
|
||||
(cwd / 'a.gif').write_bytes(b'GIF87a' + GIF_SCREEN + GIF_IMAGE)
|
||||
roundtrip(cwd, 'a.gif', '#old')
|
||||
assert (cwd / 'a.gif').read_bytes() == b'GIF89a' + GIF_SCREEN + b'\x21\xfe' + bytes([10]) + b'otag: #old\0' + GIF_IMAGE
|
||||
|
||||
|
||||
@pytest.mark.parametrize('name', ['a.jpg', 'a.png', 'a.gif'])
|
||||
def test_other_files_are_refused_unchanged(cwd, name):
|
||||
(cwd / name).write_bytes(b'plain text')
|
||||
OrvaniTag().add(name, '#lofi')
|
||||
with pytest.raises(UnsupportedFile):
|
||||
OrvaniTag().write_metadata(name, 'image')
|
||||
assert (cwd / name).read_bytes() == b'plain text'
|
||||
Loading…
Add table
Add a link
Reference in a new issue