Remove submodule, just put Dependencies in ./libs
This commit is contained in:
1 parent
2fb578d4de
commit
6888798ef6
541 files changed
+173394
-4
No files matched your search
@@ -0,0 +1,15 @@
|
||||
from hachoir_metadata.version import VERSION as __version__
|
||||
from hachoir_metadata.metadata import extractMetadata
|
||||
|
||||
# Just import the module,
|
||||
# each module use registerExtractor() method
|
||||
import hachoir_metadata.archive
|
||||
import hachoir_metadata.audio
|
||||
import hachoir_metadata.file_system
|
||||
import hachoir_metadata.image
|
||||
import hachoir_metadata.jpeg
|
||||
import hachoir_metadata.misc
|
||||
import hachoir_metadata.program
|
||||
import hachoir_metadata.riff
|
||||
import hachoir_metadata.video
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
from hachoir_metadata.metadata_item import QUALITY_BEST, QUALITY_FASTEST
|
||||
from hachoir_metadata.safe import fault_tolerant, getValue
|
||||
from hachoir_metadata.metadata import (
|
||||
RootMetadata, Metadata, MultipleMetadata, registerExtractor)
|
||||
from hachoir_parser.archive import (Bzip2Parser, CabFile, GzipParser,
|
||||
TarFile, ZipFile, MarFile)
|
||||
from hachoir_core.tools import humanUnixAttributes
|
||||
from hachoir_core.i18n import _
|
||||
|
||||
def maxNbFile(meta):
|
||||
if meta.quality <= QUALITY_FASTEST:
|
||||
return 0
|
||||
if QUALITY_BEST <= meta.quality:
|
||||
return None
|
||||
return 1 + int(10 * meta.quality)
|
||||
|
||||
def computeCompressionRate(meta):
|
||||
"""
|
||||
Compute compression rate, sizes have to be in byte.
|
||||
"""
|
||||
if not meta.has("file_size") \
|
||||
or not meta.get("compr_size", 0):
|
||||
return
|
||||
file_size = meta.get("file_size")
|
||||
if not file_size:
|
||||
return
|
||||
meta.compr_rate = float(file_size) / meta.get("compr_size")
|
||||
|
||||
class Bzip2Metadata(RootMetadata):
|
||||
def extract(self, zip):
|
||||
if "file" in zip:
|
||||
self.compr_size = zip["file"].size/8
|
||||
|
||||
class GzipMetadata(RootMetadata):
|
||||
def extract(self, gzip):
|
||||
self.useHeader(gzip)
|
||||
computeCompressionRate(self)
|
||||
|
||||
@fault_tolerant
|
||||
def useHeader(self, gzip):
|
||||
self.compression = gzip["compression"].display
|
||||
if gzip["mtime"]:
|
||||
self.last_modification = gzip["mtime"].value
|
||||
self.os = gzip["os"].display
|
||||
if gzip["has_filename"].value:
|
||||
self.filename = getValue(gzip, "filename")
|
||||
if gzip["has_comment"].value:
|
||||
self.comment = getValue(gzip, "comment")
|
||||
self.compr_size = gzip["file"].size/8
|
||||
self.file_size = gzip["size"].value
|
||||
|
||||
class ZipMetadata(MultipleMetadata):
|
||||
def extract(self, zip):
|
||||
max_nb = maxNbFile(self)
|
||||
for index, field in enumerate(zip.array("file")):
|
||||
if max_nb is not None and max_nb <= index:
|
||||
self.warning("ZIP archive contains many files, but only first %s files are processed" % max_nb)
|
||||
break
|
||||
self.processFile(field)
|
||||
|
||||
@fault_tolerant
|
||||
def processFile(self, field):
|
||||
meta = Metadata(self)
|
||||
meta.filename = field["filename"].value
|
||||
meta.creation_date = field["last_mod"].value
|
||||
meta.compression = field["compression"].display
|
||||
if "data_desc" in field:
|
||||
meta.file_size = field["data_desc/file_uncompressed_size"].value
|
||||
if field["data_desc/file_compressed_size"].value:
|
||||
meta.compr_size = field["data_desc/file_compressed_size"].value
|
||||
else:
|
||||
meta.file_size = field["uncompressed_size"].value
|
||||
if field["compressed_size"].value:
|
||||
meta.compr_size = field["compressed_size"].value
|
||||
computeCompressionRate(meta)
|
||||
self.addGroup(field.name, meta, "File \"%s\"" % meta.get('filename'))
|
||||
|
||||
class TarMetadata(MultipleMetadata):
|
||||
def extract(self, tar):
|
||||
max_nb = maxNbFile(self)
|
||||
for index, field in enumerate(tar.array("file")):
|
||||
if max_nb is not None and max_nb <= index:
|
||||
self.warning("TAR archive contains many files, but only first %s files are processed" % max_nb)
|
||||
break
|
||||
meta = Metadata(self)
|
||||
self.extractFile(field, meta)
|
||||
if meta.has("filename"):
|
||||
title = _('File "%s"') % meta.getText('filename')
|
||||
else:
|
||||
title = _("File")
|
||||
self.addGroup(field.name, meta, title)
|
||||
|
||||
@fault_tolerant
|
||||
def extractFile(self, field, meta):
|
||||
meta.filename = field["name"].value
|
||||
meta.file_attr = humanUnixAttributes(field.getOctal("mode"))
|
||||
meta.file_size = field.getOctal("size")
|
||||
try:
|
||||
if field.getOctal("mtime"):
|
||||
meta.last_modification = field.getDatetime()
|
||||
except ValueError:
|
||||
pass
|
||||
meta.file_type = field["type"].display
|
||||
meta.author = "%s (uid=%s), group %s (gid=%s)" %\
|
||||
(field["uname"].value, field.getOctal("uid"),
|
||||
field["gname"].value, field.getOctal("gid"))
|
||||
|
||||
|
||||
class CabMetadata(MultipleMetadata):
|
||||
def extract(self, cab):
|
||||
if "folder[0]" in cab:
|
||||
self.useFolder(cab["folder[0]"])
|
||||
self.format_version = "Microsoft Cabinet version %s" % cab["cab_version"].display
|
||||
self.comment = "%s folders, %s files" % (
|
||||
cab["nb_folder"].value, cab["nb_files"].value)
|
||||
max_nb = maxNbFile(self)
|
||||
for index, field in enumerate(cab.array("file")):
|
||||
if max_nb is not None and max_nb <= index:
|
||||
self.warning("CAB archive contains many files, but only first %s files are processed" % max_nb)
|
||||
break
|
||||
self.useFile(field)
|
||||
|
||||
@fault_tolerant
|
||||
def useFolder(self, folder):
|
||||
compr = folder["compr_method"].display
|
||||
if folder["compr_method"].value != 0:
|
||||
compr += " (level %u)" % folder["compr_level"].value
|
||||
self.compression = compr
|
||||
|
||||
@fault_tolerant
|
||||
def useFile(self, field):
|
||||
meta = Metadata(self)
|
||||
meta.filename = field["filename"].value
|
||||
meta.file_size = field["filesize"].value
|
||||
meta.creation_date = field["timestamp"].value
|
||||
attr = field["attributes"].value
|
||||
if attr != "(none)":
|
||||
meta.file_attr = attr
|
||||
if meta.has("filename"):
|
||||
title = _("File \"%s\"") % meta.getText('filename')
|
||||
else:
|
||||
title = _("File")
|
||||
self.addGroup(field.name, meta, title)
|
||||
|
||||
class MarMetadata(MultipleMetadata):
|
||||
def extract(self, mar):
|
||||
self.comment = "Contains %s files" % mar["nb_file"].value
|
||||
self.format_version = "Microsoft Archive version %s" % mar["version"].value
|
||||
max_nb = maxNbFile(self)
|
||||
for index, field in enumerate(mar.array("file")):
|
||||
if max_nb is not None and max_nb <= index:
|
||||
self.warning("MAR archive contains many files, but only first %s files are processed" % max_nb)
|
||||
break
|
||||
meta = Metadata(self)
|
||||
meta.filename = field["filename"].value
|
||||
meta.compression = "None"
|
||||
meta.file_size = field["filesize"].value
|
||||
self.addGroup(field.name, meta, "File \"%s\"" % meta.getText('filename'))
|
||||
|
||||
registerExtractor(CabFile, CabMetadata)
|
||||
registerExtractor(GzipParser, GzipMetadata)
|
||||
registerExtractor(Bzip2Parser, Bzip2Metadata)
|
||||
registerExtractor(TarFile, TarMetadata)
|
||||
registerExtractor(ZipFile, ZipMetadata)
|
||||
registerExtractor(MarFile, MarMetadata)
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
from hachoir_metadata.metadata import (registerExtractor,
|
||||
Metadata, RootMetadata, MultipleMetadata)
|
||||
from hachoir_parser.audio import AuFile, MpegAudioFile, RealAudioFile, AiffFile, FlacParser
|
||||
from hachoir_parser.container import OggFile, RealMediaFile
|
||||
from hachoir_core.i18n import _
|
||||
from hachoir_core.tools import makePrintable, timedelta2seconds, humanBitRate
|
||||
from datetime import timedelta
|
||||
from hachoir_metadata.metadata_item import QUALITY_FAST, QUALITY_NORMAL, QUALITY_BEST
|
||||
from hachoir_metadata.safe import fault_tolerant, getValue
|
||||
|
||||
def computeComprRate(meta, size):
|
||||
if not meta.has("duration") \
|
||||
or not meta.has("sample_rate") \
|
||||
or not meta.has("bits_per_sample") \
|
||||
or not meta.has("nb_channel") \
|
||||
or not size:
|
||||
return
|
||||
orig_size = timedelta2seconds(meta.get("duration")) * meta.get('sample_rate') * meta.get('bits_per_sample') * meta.get('nb_channel')
|
||||
meta.compr_rate = float(orig_size) / size
|
||||
|
||||
def computeBitRate(meta):
|
||||
if not meta.has("bits_per_sample") \
|
||||
or not meta.has("nb_channel") \
|
||||
or not meta.has("sample_rate"):
|
||||
return
|
||||
meta.bit_rate = meta.get('bits_per_sample') * meta.get('nb_channel') * meta.get('sample_rate')
|
||||
|
||||
VORBIS_KEY_TO_ATTR = {
|
||||
"ARTIST": "artist",
|
||||
"ALBUM": "album",
|
||||
"TRACKNUMBER": "track_number",
|
||||
"TRACKTOTAL": "track_total",
|
||||
"ENCODER": "producer",
|
||||
"TITLE": "title",
|
||||
"LOCATION": "location",
|
||||
"DATE": "creation_date",
|
||||
"ORGANIZATION": "organization",
|
||||
"GENRE": "music_genre",
|
||||
"": "comment",
|
||||
"COMPOSER": "music_composer",
|
||||
"DESCRIPTION": "comment",
|
||||
"COMMENT": "comment",
|
||||
"WWW": "url",
|
||||
"WOAF": "url",
|
||||
"LICENSE": "copyright",
|
||||
}
|
||||
|
||||
@fault_tolerant
|
||||
def readVorbisComment(metadata, comment):
|
||||
metadata.producer = getValue(comment, "vendor")
|
||||
for item in comment.array("metadata"):
|
||||
if "=" in item.value:
|
||||
key, value = item.value.split("=", 1)
|
||||
key = key.upper()
|
||||
if key in VORBIS_KEY_TO_ATTR:
|
||||
key = VORBIS_KEY_TO_ATTR[key]
|
||||
setattr(metadata, key, value)
|
||||
elif value:
|
||||
metadata.warning("Skip Vorbis comment %s: %s" % (key, value))
|
||||
|
||||
class OggMetadata(MultipleMetadata):
|
||||
def extract(self, ogg):
|
||||
granule_quotient = None
|
||||
for index, page in enumerate(ogg.array("page")):
|
||||
if "segments" not in page:
|
||||
continue
|
||||
page = page["segments"]
|
||||
if "vorbis_hdr" in page:
|
||||
meta = Metadata(self)
|
||||
self.vorbisHeader(page["vorbis_hdr"], meta)
|
||||
self.addGroup("audio[]", meta, "Audio")
|
||||
if not granule_quotient and meta.has("sample_rate"):
|
||||
granule_quotient = meta.get('sample_rate')
|
||||
if "theora_hdr" in page:
|
||||
meta = Metadata(self)
|
||||
self.theoraHeader(page["theora_hdr"], meta)
|
||||
self.addGroup("video[]", meta, "Video")
|
||||
if "video_hdr" in page:
|
||||
meta = Metadata(self)
|
||||
self.videoHeader(page["video_hdr"], meta)
|
||||
self.addGroup("video[]", meta, "Video")
|
||||
if not granule_quotient and meta.has("frame_rate"):
|
||||
granule_quotient = meta.get('frame_rate')
|
||||
if "comment" in page:
|
||||
readVorbisComment(self, page["comment"])
|
||||
if 3 <= index:
|
||||
# Only process pages 0..3
|
||||
break
|
||||
|
||||
# Compute duration
|
||||
if granule_quotient and QUALITY_NORMAL <= self.quality:
|
||||
page = ogg.createLastPage()
|
||||
if page and "abs_granule_pos" in page:
|
||||
try:
|
||||
self.duration = timedelta(seconds=float(page["abs_granule_pos"].value) / granule_quotient)
|
||||
except OverflowError:
|
||||
pass
|
||||
|
||||
def videoHeader(self, header, meta):
|
||||
meta.compression = header["fourcc"].display
|
||||
meta.width = header["width"].value
|
||||
meta.height = header["height"].value
|
||||
meta.bits_per_pixel = header["bits_per_sample"].value
|
||||
if header["time_unit"].value:
|
||||
meta.frame_rate = 10000000.0 / header["time_unit"].value
|
||||
|
||||
def theoraHeader(self, header, meta):
|
||||
meta.compression = "Theora"
|
||||
meta.format_version = "Theora version %u.%u (revision %u)" % (\
|
||||
header["version_major"].value,
|
||||
header["version_minor"].value,
|
||||
header["version_revision"].value)
|
||||
meta.width = header["frame_width"].value
|
||||
meta.height = header["frame_height"].value
|
||||
if header["fps_den"].value:
|
||||
meta.frame_rate = float(header["fps_num"].value) / header["fps_den"].value
|
||||
if header["aspect_ratio_den"].value:
|
||||
meta.aspect_ratio = float(header["aspect_ratio_num"].value) / header["aspect_ratio_den"].value
|
||||
meta.pixel_format = header["pixel_format"].display
|
||||
meta.comment = "Quality: %s" % header["quality"].value
|
||||
|
||||
def vorbisHeader(self, header, meta):
|
||||
meta.compression = u"Vorbis"
|
||||
meta.sample_rate = header["audio_sample_rate"].value
|
||||
meta.nb_channel = header["audio_channels"].value
|
||||
meta.format_version = u"Vorbis version %s" % header["vorbis_version"].value
|
||||
meta.bit_rate = header["bitrate_nominal"].value
|
||||
|
||||
class AuMetadata(RootMetadata):
|
||||
def extract(self, audio):
|
||||
self.sample_rate = audio["sample_rate"].value
|
||||
self.nb_channel = audio["channels"].value
|
||||
self.compression = audio["codec"].display
|
||||
if "info" in audio:
|
||||
self.comment = audio["info"].value
|
||||
self.bits_per_sample = audio.getBitsPerSample()
|
||||
computeBitRate(self)
|
||||
if "audio_data" in audio:
|
||||
if self.has("bit_rate"):
|
||||
self.duration = timedelta(seconds=float(audio["audio_data"].size) / self.get('bit_rate'))
|
||||
computeComprRate(self, audio["audio_data"].size)
|
||||
|
||||
class RealAudioMetadata(RootMetadata):
|
||||
FOURCC_TO_BITRATE = {
|
||||
u"28_8": 15200, # 28.8 kbit/sec (audio bit rate: 15.2 kbit/s)
|
||||
u"14_4": 8000, # 14.4 kbit/sec
|
||||
u"lpcJ": 8000, # 14.4 kbit/sec
|
||||
}
|
||||
|
||||
def extract(self, real):
|
||||
version = real["version"].value
|
||||
if "metadata" in real:
|
||||
self.useMetadata(real["metadata"])
|
||||
self.useRoot(real)
|
||||
self.format_version = "Real audio version %s" % version
|
||||
if version == 3:
|
||||
size = getValue(real, "data_size")
|
||||
elif "filesize" in real and "headersize" in real:
|
||||
size = (real["filesize"].value + 40) - (real["headersize"].value + 16)
|
||||
else:
|
||||
size = None
|
||||
if size:
|
||||
size *= 8
|
||||
if self.has("bit_rate"):
|
||||
sec = float(size) / self.get('bit_rate')
|
||||
self.duration = timedelta(seconds=sec)
|
||||
computeComprRate(self, size)
|
||||
|
||||
@fault_tolerant
|
||||
def useMetadata(self, info):
|
||||
self.title = info["title"].value
|
||||
self.author = info["author"].value
|
||||
self.copyright = info["copyright"].value
|
||||
self.comment = info["comment"].value
|
||||
|
||||
@fault_tolerant
|
||||
def useRoot(self, real):
|
||||
self.bits_per_sample = 16 # FIXME: Is that correct?
|
||||
if real["version"].value != 3:
|
||||
self.sample_rate = real["sample_rate"].value
|
||||
self.nb_channel = real["channels"].value
|
||||
else:
|
||||
self.sample_rate = 8000
|
||||
self.nb_channel = 1
|
||||
fourcc = getValue(real, "FourCC")
|
||||
if fourcc:
|
||||
self.compression = fourcc
|
||||
try:
|
||||
self.bit_rate = self.FOURCC_TO_BITRATE[fourcc]
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
class RealMediaMetadata(MultipleMetadata):
|
||||
KEY_TO_ATTR = {
|
||||
"generated by": "producer",
|
||||
"creation date": "creation_date",
|
||||
"modification date": "last_modification",
|
||||
"description": "comment",
|
||||
}
|
||||
|
||||
def extract(self, media):
|
||||
if "file_prop" in media:
|
||||
self.useFileProp(media["file_prop"])
|
||||
if "content_desc" in media:
|
||||
self.useContentDesc(media["content_desc"])
|
||||
for index, stream in enumerate(media.array("stream_prop")):
|
||||
self.useStreamProp(stream, index)
|
||||
|
||||
@fault_tolerant
|
||||
def useFileInfoProp(self, prop):
|
||||
key = prop["name"].value.lower()
|
||||
value = prop["value"].value
|
||||
if key in self.KEY_TO_ATTR:
|
||||
setattr(self, self.KEY_TO_ATTR[key], value)
|
||||
elif value:
|
||||
self.warning("Skip %s: %s" % (prop["name"].value, value))
|
||||
|
||||
@fault_tolerant
|
||||
def useFileProp(self, prop):
|
||||
self.bit_rate = prop["avg_bit_rate"].value
|
||||
self.duration = timedelta(milliseconds=prop["duration"].value)
|
||||
|
||||
@fault_tolerant
|
||||
def useContentDesc(self, content):
|
||||
self.title = content["title"].value
|
||||
self.author = content["author"].value
|
||||
self.copyright = content["copyright"].value
|
||||
self.comment = content["comment"].value
|
||||
|
||||
@fault_tolerant
|
||||
def useStreamProp(self, stream, index):
|
||||
meta = Metadata(self)
|
||||
meta.comment = "Start: %s" % stream["stream_start"].value
|
||||
if getValue(stream, "mime_type") == "logical-fileinfo":
|
||||
for prop in stream.array("file_info/prop"):
|
||||
self.useFileInfoProp(prop)
|
||||
else:
|
||||
meta.bit_rate = stream["avg_bit_rate"].value
|
||||
meta.duration = timedelta(milliseconds=stream["duration"].value)
|
||||
meta.mime_type = getValue(stream, "mime_type")
|
||||
meta.title = getValue(stream, "desc")
|
||||
self.addGroup("stream[%u]" % index, meta, "Stream #%u" % (1+index))
|
||||
|
||||
class MpegAudioMetadata(RootMetadata):
|
||||
TAG_TO_KEY = {
|
||||
# ID3 version 2.2
|
||||
"TP1": "author",
|
||||
"COM": "comment",
|
||||
"TEN": "producer",
|
||||
"TRK": "track_number",
|
||||
"TAL": "album",
|
||||
"TT2": "title",
|
||||
"TYE": "creation_date",
|
||||
"TCO": "music_genre",
|
||||
|
||||
# ID3 version 2.3+
|
||||
"TPE1": "author",
|
||||
"COMM": "comment",
|
||||
"TENC": "producer",
|
||||
"TRCK": "track_number",
|
||||
"TALB": "album",
|
||||
"TIT2": "title",
|
||||
"TYER": "creation_date",
|
||||
"WXXX": "url",
|
||||
"TCON": "music_genre",
|
||||
"TLAN": "language",
|
||||
"TCOP": "copyright",
|
||||
"TDAT": "creation_date",
|
||||
"TRDA": "creation_date",
|
||||
"TORY": "creation_date",
|
||||
"TIT1": "title",
|
||||
}
|
||||
|
||||
def processID3v2(self, field):
|
||||
# Read value
|
||||
if "content" not in field:
|
||||
return
|
||||
content = field["content"]
|
||||
if "text" not in content:
|
||||
return
|
||||
if "title" in content and content["title"].value:
|
||||
value = "%s: %s" % (content["title"].value, content["text"].value)
|
||||
else:
|
||||
value = content["text"].value
|
||||
|
||||
# Known tag?
|
||||
tag = field["tag"].value
|
||||
if tag not in self.TAG_TO_KEY:
|
||||
if tag:
|
||||
if isinstance(tag, str):
|
||||
tag = makePrintable(tag, "ISO-8859-1", to_unicode=True)
|
||||
self.warning("Skip ID3v2 tag %s: %s" % (tag, value))
|
||||
return
|
||||
key = self.TAG_TO_KEY[tag]
|
||||
setattr(self, key, value)
|
||||
|
||||
def readID3v2(self, id3):
|
||||
for field in id3:
|
||||
if field.is_field_set and "tag" in field:
|
||||
self.processID3v2(field)
|
||||
|
||||
def extract(self, mp3):
|
||||
if "/frames/frame[0]" in mp3:
|
||||
frame = mp3["/frames/frame[0]"]
|
||||
self.nb_channel = (frame.getNbChannel(), frame["channel_mode"].display)
|
||||
self.format_version = u"MPEG version %s layer %s" % \
|
||||
(frame["version"].display, frame["layer"].display)
|
||||
self.sample_rate = frame.getSampleRate()
|
||||
self.bits_per_sample = 16
|
||||
if mp3["frames"].looksConstantBitRate():
|
||||
self.computeBitrate(frame)
|
||||
else:
|
||||
self.computeVariableBitrate(mp3)
|
||||
if "id3v1" in mp3:
|
||||
id3 = mp3["id3v1"]
|
||||
self.comment = id3["comment"].value
|
||||
self.author = id3["author"].value
|
||||
self.title = id3["song"].value
|
||||
self.album = id3["album"].value
|
||||
if id3["year"].value != "0":
|
||||
self.creation_date = id3["year"].value
|
||||
if "track_nb" in id3:
|
||||
self.track_number = id3["track_nb"].value
|
||||
if "id3v2" in mp3:
|
||||
self.readID3v2(mp3["id3v2"])
|
||||
if "frames" in mp3:
|
||||
computeComprRate(self, mp3["frames"].size)
|
||||
|
||||
def computeBitrate(self, frame):
|
||||
bit_rate = frame.getBitRate() # may returns None on error
|
||||
if not bit_rate:
|
||||
return
|
||||
self.bit_rate = (bit_rate, _("%s (constant)") % humanBitRate(bit_rate))
|
||||
self.duration = timedelta(seconds=float(frame["/frames"].size) / bit_rate)
|
||||
|
||||
def computeVariableBitrate(self, mp3):
|
||||
if self.quality <= QUALITY_FAST:
|
||||
return
|
||||
count = 0
|
||||
if QUALITY_BEST <= self.quality:
|
||||
self.warning("Process all MPEG audio frames to compute exact duration")
|
||||
max_count = None
|
||||
else:
|
||||
max_count = 500 * self.quality
|
||||
total_bit_rate = 0.0
|
||||
for index, frame in enumerate(mp3.array("frames/frame")):
|
||||
if index < 3:
|
||||
continue
|
||||
bit_rate = frame.getBitRate()
|
||||
if bit_rate:
|
||||
total_bit_rate += float(bit_rate)
|
||||
count += 1
|
||||
if max_count and max_count <= count:
|
||||
break
|
||||
if not count:
|
||||
return
|
||||
bit_rate = total_bit_rate / count
|
||||
self.bit_rate = (bit_rate,
|
||||
_("%s (Variable bit rate)") % humanBitRate(bit_rate))
|
||||
duration = timedelta(seconds=float(mp3["frames"].size) / bit_rate)
|
||||
self.duration = duration
|
||||
|
||||
class AiffMetadata(RootMetadata):
|
||||
def extract(self, aiff):
|
||||
if "common" in aiff:
|
||||
self.useCommon(aiff["common"])
|
||||
computeBitRate(self)
|
||||
|
||||
@fault_tolerant
|
||||
def useCommon(self, info):
|
||||
self.nb_channel = info["nb_channel"].value
|
||||
self.bits_per_sample = info["sample_size"].value
|
||||
self.sample_rate = getValue(info, "sample_rate")
|
||||
if self.has("sample_rate"):
|
||||
rate = self.get("sample_rate")
|
||||
if rate:
|
||||
sec = float(info["nb_sample"].value) / rate
|
||||
self.duration = timedelta(seconds=sec)
|
||||
if "codec" in info:
|
||||
self.compression = info["codec"].display
|
||||
|
||||
class FlacMetadata(RootMetadata):
|
||||
def extract(self, flac):
|
||||
if "metadata/stream_info/content" in flac:
|
||||
self.useStreamInfo(flac["metadata/stream_info/content"])
|
||||
if "metadata/comment/content" in flac:
|
||||
readVorbisComment(self, flac["metadata/comment/content"])
|
||||
|
||||
@fault_tolerant
|
||||
def useStreamInfo(self, info):
|
||||
self.nb_channel = info["nb_channel"].value + 1
|
||||
self.bits_per_sample = info["bits_per_sample"].value + 1
|
||||
self.sample_rate = info["sample_hertz"].value
|
||||
sec = info["total_samples"].value
|
||||
if sec:
|
||||
sec = float(sec) / info["sample_hertz"].value
|
||||
self.duration = timedelta(seconds=sec)
|
||||
|
||||
registerExtractor(AuFile, AuMetadata)
|
||||
registerExtractor(MpegAudioFile, MpegAudioMetadata)
|
||||
registerExtractor(OggFile, OggMetadata)
|
||||
registerExtractor(RealMediaFile, RealMediaMetadata)
|
||||
registerExtractor(RealAudioFile, RealAudioMetadata)
|
||||
registerExtractor(AiffFile, AiffMetadata)
|
||||
registerExtractor(FlacParser, FlacMetadata)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
MAX_STR_LENGTH = 300 # characters
|
||||
RAW_OUTPUT = False
|
||||
@@ -0,0 +1,28 @@
|
||||
from hachoir_metadata.metadata import RootMetadata, registerExtractor
|
||||
from hachoir_metadata.safe import fault_tolerant
|
||||
from hachoir_parser.file_system import ISO9660
|
||||
from datetime import datetime
|
||||
|
||||
class ISO9660_Metadata(RootMetadata):
|
||||
def extract(self, iso):
|
||||
desc = iso['volume[0]/content']
|
||||
self.title = desc['volume_id'].value
|
||||
self.title = desc['vol_set_id'].value
|
||||
self.author = desc['publisher'].value
|
||||
self.author = desc['data_preparer'].value
|
||||
self.producer = desc['application'].value
|
||||
self.copyright = desc['copyright'].value
|
||||
self.readTimestamp('creation_date', desc['creation_ts'].value)
|
||||
self.readTimestamp('last_modification', desc['modification_ts'].value)
|
||||
|
||||
@fault_tolerant
|
||||
def readTimestamp(self, key, value):
|
||||
if value.startswith("0000"):
|
||||
return
|
||||
value = datetime(
|
||||
int(value[0:4]), int(value[4:6]), int(value[6:8]),
|
||||
int(value[8:10]), int(value[10:12]), int(value[12:14]))
|
||||
setattr(self, key, value)
|
||||
|
||||
registerExtractor(ISO9660, ISO9660_Metadata)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
from hachoir_metadata.timezone import UTC
|
||||
from datetime import date, datetime
|
||||
|
||||
# Year in 1850..2030
|
||||
MIN_YEAR = 1850
|
||||
MAX_YEAR = 2030
|
||||
|
||||
class Filter:
|
||||
def __init__(self, valid_types, min=None, max=None):
|
||||
self.types = valid_types
|
||||
self.min = min
|
||||
self.max = max
|
||||
|
||||
def __call__(self, value):
|
||||
if not isinstance(value, self.types):
|
||||
return True
|
||||
if self.min is not None and value < self.min:
|
||||
return False
|
||||
if self.max is not None and self.max < value:
|
||||
return False
|
||||
return True
|
||||
|
||||
class NumberFilter(Filter):
|
||||
def __init__(self, min=None, max=None):
|
||||
Filter.__init__(self, (int, long, float), min, max)
|
||||
|
||||
class DatetimeFilter(Filter):
|
||||
def __init__(self, min=None, max=None):
|
||||
Filter.__init__(self, (date, datetime),
|
||||
datetime(MIN_YEAR, 1, 1),
|
||||
datetime(MAX_YEAR, 12, 31))
|
||||
self.min_date = date(MIN_YEAR, 1, 1)
|
||||
self.max_date = date(MAX_YEAR, 12, 31)
|
||||
self.min_tz = datetime(MIN_YEAR, 1, 1, tzinfo=UTC)
|
||||
self.max_tz = datetime(MAX_YEAR, 12, 31, tzinfo=UTC)
|
||||
|
||||
def __call__(self, value):
|
||||
"""
|
||||
Use different min/max values depending on value type
|
||||
(datetime with timezone, datetime or date).
|
||||
"""
|
||||
if not isinstance(value, self.types):
|
||||
return True
|
||||
if hasattr(value, "tzinfo") and value.tzinfo:
|
||||
return (self.min_tz <= value <= self.max_tz)
|
||||
elif isinstance(value, datetime):
|
||||
return (self.min <= value <= self.max)
|
||||
else:
|
||||
return (self.min_date <= value <= self.max_date)
|
||||
|
||||
DATETIME_FILTER = DatetimeFilter()
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from hachoir_core.i18n import _, ngettext
|
||||
|
||||
NB_CHANNEL_NAME = {1: _("mono"), 2: _("stereo")}
|
||||
|
||||
def humanAudioChannel(value):
|
||||
return NB_CHANNEL_NAME.get(value, unicode(value))
|
||||
|
||||
def humanFrameRate(value):
|
||||
if isinstance(value, (int, long, float)):
|
||||
return _("%.1f fps") % value
|
||||
else:
|
||||
return value
|
||||
|
||||
def humanComprRate(rate):
|
||||
return u"%.1fx" % rate
|
||||
|
||||
def humanAltitude(value):
|
||||
return ngettext("%.1f meter", "%.1f meters", value) % value
|
||||
|
||||
def humanPixelSize(value):
|
||||
return ngettext("%s pixel", "%s pixels", value) % value
|
||||
|
||||
def humanDPI(value):
|
||||
return u"%s DPI" % value
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
from hachoir_metadata.metadata import (registerExtractor,
|
||||
Metadata, RootMetadata, MultipleMetadata)
|
||||
from hachoir_parser.image import (
|
||||
BmpFile, IcoFile, PcxFile, GifFile, PngFile, TiffFile,
|
||||
XcfFile, TargaFile, WMF_File, PsdFile)
|
||||
from hachoir_parser.image.png import getBitsPerPixel as pngBitsPerPixel
|
||||
from hachoir_parser.image.xcf import XcfProperty
|
||||
from hachoir_core.i18n import _
|
||||
from hachoir_metadata.safe import fault_tolerant
|
||||
|
||||
def computeComprRate(meta, compr_size):
|
||||
"""
|
||||
Compute image compression rate. Skip size of color palette, focus on
|
||||
image pixels. Original size is width x height x bpp. Compressed size
|
||||
is an argument (in bits).
|
||||
|
||||
Set "compr_data" with a string like "1.52x".
|
||||
"""
|
||||
if not meta.has("width") \
|
||||
or not meta.has("height") \
|
||||
or not meta.has("bits_per_pixel"):
|
||||
return
|
||||
if not compr_size:
|
||||
return
|
||||
orig_size = meta.get('width') * meta.get('height') * meta.get('bits_per_pixel')
|
||||
meta.compr_rate = float(orig_size) / compr_size
|
||||
|
||||
class BmpMetadata(RootMetadata):
|
||||
def extract(self, image):
|
||||
if "header" not in image:
|
||||
return
|
||||
hdr = image["header"]
|
||||
self.width = hdr["width"].value
|
||||
self.height = hdr["height"].value
|
||||
bpp = hdr["bpp"].value
|
||||
if bpp:
|
||||
if bpp <= 8 and "used_colors" in hdr:
|
||||
self.nb_colors = hdr["used_colors"].value
|
||||
self.bits_per_pixel = bpp
|
||||
self.compression = hdr["compression"].display
|
||||
self.format_version = u"Microsoft Bitmap version %s" % hdr.getFormatVersion()
|
||||
|
||||
self.width_dpi = hdr["horizontal_dpi"].value
|
||||
self.height_dpi = hdr["vertical_dpi"].value
|
||||
|
||||
if "pixels" in image:
|
||||
computeComprRate(self, image["pixels"].size)
|
||||
|
||||
class TiffMetadata(RootMetadata):
|
||||
key_to_attr = {
|
||||
"img_width": "width",
|
||||
"img_height": "width",
|
||||
|
||||
# TODO: Enable that (need link to value)
|
||||
# "description": "comment",
|
||||
# "doc_name": "title",
|
||||
# "orientation": "image_orientation",
|
||||
}
|
||||
def extract(self, tiff):
|
||||
if "ifd" in tiff:
|
||||
self.useIFD(tiff["ifd"])
|
||||
|
||||
def useIFD(self, ifd):
|
||||
for field in ifd:
|
||||
try:
|
||||
attrname = self.key_to_attr[field.name]
|
||||
except KeyError:
|
||||
continue
|
||||
if "value" not in field:
|
||||
continue
|
||||
value = field["value"].value
|
||||
setattr(self, attrname, value)
|
||||
|
||||
class IcoMetadata(MultipleMetadata):
|
||||
color_to_bpp = {
|
||||
2: 1,
|
||||
16: 4,
|
||||
256: 8
|
||||
}
|
||||
|
||||
def extract(self, icon):
|
||||
for index, header in enumerate(icon.array("icon_header")):
|
||||
image = Metadata(self)
|
||||
|
||||
# Read size and colors from header
|
||||
image.width = header["width"].value
|
||||
image.height = header["height"].value
|
||||
bpp = header["bpp"].value
|
||||
nb_colors = header["nb_color"].value
|
||||
if nb_colors != 0:
|
||||
image.nb_colors = nb_colors
|
||||
if bpp == 0 and nb_colors in self.color_to_bpp:
|
||||
bpp = self.color_to_bpp[nb_colors]
|
||||
elif bpp == 0:
|
||||
bpp = 8
|
||||
image.bits_per_pixel = bpp
|
||||
image.setHeader(_("Icon #%u (%sx%s)")
|
||||
% (1+index, image.get("width", "?"), image.get("height", "?")))
|
||||
|
||||
# Read compression from data (if available)
|
||||
key = "icon_data[%u]/header/codec" % index
|
||||
if key in icon:
|
||||
image.compression = icon[key].display
|
||||
key = "icon_data[%u]/pixels" % index
|
||||
if key in icon:
|
||||
computeComprRate(image, icon[key].size)
|
||||
|
||||
# Store new image
|
||||
self.addGroup("image[%u]" % index, image)
|
||||
|
||||
class PcxMetadata(RootMetadata):
|
||||
@fault_tolerant
|
||||
def extract(self, pcx):
|
||||
self.width = 1 + pcx["xmax"].value
|
||||
self.height = 1 + pcx["ymax"].value
|
||||
self.width_dpi = pcx["horiz_dpi"].value
|
||||
self.height_dpi = pcx["vert_dpi"].value
|
||||
self.bits_per_pixel = pcx["bpp"].value
|
||||
if 1 <= pcx["bpp"].value <= 8:
|
||||
self.nb_colors = 2 ** pcx["bpp"].value
|
||||
self.compression = _("Run-length encoding (RLE)")
|
||||
self.format_version = "PCX: %s" % pcx["version"].display
|
||||
if "image_data" in pcx:
|
||||
computeComprRate(self, pcx["image_data"].size)
|
||||
|
||||
class XcfMetadata(RootMetadata):
|
||||
# Map image type to bits/pixel
|
||||
TYPE_TO_BPP = {0: 24, 1: 8, 2: 8}
|
||||
|
||||
def extract(self, xcf):
|
||||
self.width = xcf["width"].value
|
||||
self.height = xcf["height"].value
|
||||
try:
|
||||
self.bits_per_pixel = self.TYPE_TO_BPP[ xcf["type"].value ]
|
||||
except KeyError:
|
||||
pass
|
||||
self.format_version = xcf["type"].display
|
||||
self.readProperties(xcf)
|
||||
|
||||
@fault_tolerant
|
||||
def processProperty(self, prop):
|
||||
type = prop["type"].value
|
||||
if type == XcfProperty.PROP_PARASITES:
|
||||
for field in prop["data"]:
|
||||
if "name" not in field or "data" not in field:
|
||||
continue
|
||||
if field["name"].value == "gimp-comment":
|
||||
self.comment = field["data"].value
|
||||
elif type == XcfProperty.PROP_COMPRESSION:
|
||||
self.compression = prop["data/compression"].display
|
||||
elif type == XcfProperty.PROP_RESOLUTION:
|
||||
self.width_dpi = int(prop["data/xres"].value)
|
||||
self.height_dpi = int(prop["data/yres"].value)
|
||||
|
||||
def readProperties(self, xcf):
|
||||
for prop in xcf.array("property"):
|
||||
self.processProperty(prop)
|
||||
|
||||
class PngMetadata(RootMetadata):
|
||||
TEXT_TO_ATTR = {
|
||||
"software": "producer",
|
||||
}
|
||||
|
||||
def extract(self, png):
|
||||
if "header" in png:
|
||||
self.useHeader(png["header"])
|
||||
if "time" in png:
|
||||
self.useTime(png["time"])
|
||||
if "physical" in png:
|
||||
self.usePhysical(png["physical"])
|
||||
for comment in png.array("text"):
|
||||
if "text" not in comment:
|
||||
continue
|
||||
keyword = comment["keyword"].value
|
||||
text = comment["text"].value
|
||||
try:
|
||||
key = self.TEXT_TO_ATTR[keyword.lower()]
|
||||
setattr(self, key, text)
|
||||
except KeyError:
|
||||
if keyword.lower() != "comment":
|
||||
self.comment = "%s=%s" % (keyword, text)
|
||||
else:
|
||||
self.comment = text
|
||||
compr_size = sum( data.size for data in png.array("data") )
|
||||
computeComprRate(self, compr_size)
|
||||
|
||||
@fault_tolerant
|
||||
def useTime(self, field):
|
||||
self.creation_date = field.value
|
||||
|
||||
@fault_tolerant
|
||||
def usePhysical(self, field):
|
||||
self.width_dpi = field["pixel_per_unit_x"].value
|
||||
self.height_dpi = field["pixel_per_unit_y"].value
|
||||
|
||||
@fault_tolerant
|
||||
def useHeader(self, header):
|
||||
self.width = header["width"].value
|
||||
self.height = header["height"].value
|
||||
|
||||
# Read number of colors and pixel format
|
||||
if "/palette/size" in header:
|
||||
nb_colors = header["/palette/size"].value // 3
|
||||
else:
|
||||
nb_colors = None
|
||||
if not header["has_palette"].value:
|
||||
if header["has_alpha"].value:
|
||||
self.pixel_format = _("RGBA")
|
||||
else:
|
||||
self.pixel_format = _("RGB")
|
||||
elif "/transparency" in header:
|
||||
self.pixel_format = _("Color index with transparency")
|
||||
if nb_colors:
|
||||
nb_colors -= 1
|
||||
else:
|
||||
self.pixel_format = _("Color index")
|
||||
self.bits_per_pixel = pngBitsPerPixel(header)
|
||||
if nb_colors:
|
||||
self.nb_colors = nb_colors
|
||||
|
||||
# Read compression, timestamp, etc.
|
||||
self.compression = header["compression"].display
|
||||
|
||||
class GifMetadata(RootMetadata):
|
||||
def extract(self, gif):
|
||||
self.useScreen(gif["/screen"])
|
||||
if self.has("bits_per_pixel"):
|
||||
self.nb_colors = (1 << self.get('bits_per_pixel'))
|
||||
self.compression = _("LZW")
|
||||
self.format_version = "GIF version %s" % gif["version"].value
|
||||
for comments in gif.array("comments"):
|
||||
for comment in gif.array(comments.name + "/comment"):
|
||||
self.comment = comment.value
|
||||
if "graphic_ctl/has_transp" in gif and gif["graphic_ctl/has_transp"].value:
|
||||
self.pixel_format = _("Color index with transparency")
|
||||
else:
|
||||
self.pixel_format = _("Color index")
|
||||
|
||||
@fault_tolerant
|
||||
def useScreen(self, screen):
|
||||
self.width = screen["width"].value
|
||||
self.height = screen["height"].value
|
||||
self.bits_per_pixel = (1 + screen["bpp"].value)
|
||||
|
||||
class TargaMetadata(RootMetadata):
|
||||
def extract(self, tga):
|
||||
self.width = tga["width"].value
|
||||
self.height = tga["height"].value
|
||||
self.bits_per_pixel = tga["bpp"].value
|
||||
if tga["nb_color"].value:
|
||||
self.nb_colors = tga["nb_color"].value
|
||||
self.compression = tga["codec"].display
|
||||
if "pixels" in tga:
|
||||
computeComprRate(self, tga["pixels"].size)
|
||||
|
||||
class WmfMetadata(RootMetadata):
|
||||
def extract(self, wmf):
|
||||
if wmf.isAPM():
|
||||
if "amf_header/rect" in wmf:
|
||||
rect = wmf["amf_header/rect"]
|
||||
self.width = (rect["right"].value - rect["left"].value)
|
||||
self.height = (rect["bottom"].value - rect["top"].value)
|
||||
self.bits_per_pixel = 24
|
||||
elif wmf.isEMF():
|
||||
emf = wmf["emf_header"]
|
||||
if "description" in emf:
|
||||
desc = emf["description"].value
|
||||
if "\0" in desc:
|
||||
self.producer, self.title = desc.split("\0", 1)
|
||||
else:
|
||||
self.producer = desc
|
||||
if emf["nb_colors"].value:
|
||||
self.nb_colors = emf["nb_colors"].value
|
||||
self.bits_per_pixel = 8
|
||||
else:
|
||||
self.bits_per_pixel = 24
|
||||
self.width = emf["width_px"].value
|
||||
self.height = emf["height_px"].value
|
||||
|
||||
class PsdMetadata(RootMetadata):
|
||||
@fault_tolerant
|
||||
def extract(self, psd):
|
||||
self.width = psd["width"].value
|
||||
self.height = psd["height"].value
|
||||
self.bits_per_pixel = psd["depth"].value * psd["nb_channels"].value
|
||||
self.pixel_format = psd["color_mode"].display
|
||||
self.compression = psd["compression"].display
|
||||
|
||||
registerExtractor(IcoFile, IcoMetadata)
|
||||
registerExtractor(GifFile, GifMetadata)
|
||||
registerExtractor(XcfFile, XcfMetadata)
|
||||
registerExtractor(TargaFile, TargaMetadata)
|
||||
registerExtractor(PcxFile, PcxMetadata)
|
||||
registerExtractor(BmpFile, BmpMetadata)
|
||||
registerExtractor(PngFile, PngMetadata)
|
||||
registerExtractor(TiffFile, TiffMetadata)
|
||||
registerExtractor(WMF_File, WmfMetadata)
|
||||
registerExtractor(PsdFile, PsdMetadata)
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
from hachoir_metadata.metadata import RootMetadata, registerExtractor
|
||||
from hachoir_metadata.image import computeComprRate
|
||||
from hachoir_parser.image.exif import ExifEntry
|
||||
from hachoir_parser.image.jpeg import (
|
||||
JpegFile, JpegChunk,
|
||||
QUALITY_HASH_COLOR, QUALITY_SUM_COLOR,
|
||||
QUALITY_HASH_GRAY, QUALITY_SUM_GRAY)
|
||||
from hachoir_core.field import MissingField
|
||||
from hachoir_core.i18n import _
|
||||
from hachoir_core.tools import makeUnicode
|
||||
from hachoir_metadata.safe import fault_tolerant
|
||||
from datetime import datetime
|
||||
|
||||
def deg2float(degree, minute, second):
|
||||
return degree + (float(minute) + float(second) / 60.0) / 60.0
|
||||
|
||||
class JpegMetadata(RootMetadata):
|
||||
EXIF_KEY = {
|
||||
# Exif metadatas
|
||||
ExifEntry.TAG_CAMERA_MANUFACTURER: "camera_manufacturer",
|
||||
ExifEntry.TAG_CAMERA_MODEL: "camera_model",
|
||||
ExifEntry.TAG_ORIENTATION: "image_orientation",
|
||||
ExifEntry.TAG_EXPOSURE: "camera_exposure",
|
||||
ExifEntry.TAG_FOCAL: "camera_focal",
|
||||
ExifEntry.TAG_BRIGHTNESS: "camera_brightness",
|
||||
ExifEntry.TAG_APERTURE: "camera_aperture",
|
||||
|
||||
# Generic metadatas
|
||||
ExifEntry.TAG_IMG_TITLE: "title",
|
||||
ExifEntry.TAG_SOFTWARE: "producer",
|
||||
ExifEntry.TAG_FILE_TIMESTAMP: "creation_date",
|
||||
ExifEntry.TAG_WIDTH: "width",
|
||||
ExifEntry.TAG_HEIGHT: "height",
|
||||
ExifEntry.TAG_USER_COMMENT: "comment",
|
||||
}
|
||||
|
||||
IPTC_KEY = {
|
||||
80: "author",
|
||||
90: "city",
|
||||
101: "country",
|
||||
116: "copyright",
|
||||
120: "title",
|
||||
231: "comment",
|
||||
}
|
||||
|
||||
orientation_name = {
|
||||
1: _('Horizontal (normal)'),
|
||||
2: _('Mirrored horizontal'),
|
||||
3: _('Rotated 180'),
|
||||
4: _('Mirrored vertical'),
|
||||
5: _('Mirrored horizontal then rotated 90 counter-clock-wise'),
|
||||
6: _('Rotated 90 clock-wise'),
|
||||
7: _('Mirrored horizontal then rotated 90 clock-wise'),
|
||||
8: _('Rotated 90 counter clock-wise'),
|
||||
}
|
||||
|
||||
def extract(self, jpeg):
|
||||
if "start_frame/content" in jpeg:
|
||||
self.startOfFrame(jpeg["start_frame/content"])
|
||||
elif "start_scan/content/nr_components" in jpeg:
|
||||
self.bits_per_pixel = 8 * jpeg["start_scan/content/nr_components"].value
|
||||
if "app0/content" in jpeg:
|
||||
self.extractAPP0(jpeg["app0/content"])
|
||||
|
||||
if "exif/content" in jpeg:
|
||||
for ifd in jpeg.array("exif/content/ifd"):
|
||||
for entry in ifd.array("entry"):
|
||||
self.processIfdEntry(ifd, entry)
|
||||
self.readGPS(ifd)
|
||||
if "photoshop/content" in jpeg:
|
||||
psd = jpeg["photoshop/content"]
|
||||
if "version/content/reader_name" in psd:
|
||||
self.producer = psd["version/content/reader_name"].value
|
||||
if "iptc/content" in psd:
|
||||
self.parseIPTC(psd["iptc/content"])
|
||||
for field in jpeg.array("comment"):
|
||||
if "content/comment" in field:
|
||||
self.comment = field["content/comment"].value
|
||||
self.computeQuality(jpeg)
|
||||
if "data" in jpeg:
|
||||
computeComprRate(self, jpeg["data"].size)
|
||||
if not self.has("producer") and "photoshop" in jpeg:
|
||||
self.producer = u"Adobe Photoshop"
|
||||
if self.has("compression"):
|
||||
self.compression = "JPEG"
|
||||
|
||||
@fault_tolerant
|
||||
def startOfFrame(self, sof):
|
||||
# Set compression method
|
||||
key = sof["../type"].value
|
||||
self.compression = "JPEG (%s)" % JpegChunk.START_OF_FRAME[key]
|
||||
|
||||
# Read image size and bits/pixel
|
||||
self.width = sof["width"].value
|
||||
self.height = sof["height"].value
|
||||
nb_components = sof["nr_components"].value
|
||||
self.bits_per_pixel = 8 * nb_components
|
||||
if nb_components == 3:
|
||||
self.pixel_format = _("YCbCr")
|
||||
elif nb_components == 1:
|
||||
self.pixel_format = _("Grayscale")
|
||||
self.nb_colors = 256
|
||||
|
||||
@fault_tolerant
|
||||
def computeQuality(self, jpeg):
|
||||
# This function is an adaption to Python of ImageMagick code
|
||||
# to compute JPEG quality using quantization tables
|
||||
|
||||
# Read quantization tables
|
||||
qtlist = []
|
||||
for dqt in jpeg.array("quantization"):
|
||||
for qt in dqt.array("content/qt"):
|
||||
# TODO: Take care of qt["index"].value?
|
||||
qtlist.append(qt)
|
||||
if not qtlist:
|
||||
return
|
||||
|
||||
# Compute sum of all coefficients
|
||||
sumcoeff = 0
|
||||
for qt in qtlist:
|
||||
coeff = qt.array("coeff")
|
||||
for index in xrange(64):
|
||||
sumcoeff += coeff[index].value
|
||||
|
||||
# Choose the right quality table and compute hash value
|
||||
try:
|
||||
hashval= qtlist[0]["coeff[2]"].value + qtlist[0]["coeff[53]"].value
|
||||
if 2 <= len(qtlist):
|
||||
hashval += qtlist[1]["coeff[0]"].value + qtlist[1]["coeff[63]"].value
|
||||
hashtable = QUALITY_HASH_COLOR
|
||||
sumtable = QUALITY_SUM_COLOR
|
||||
else:
|
||||
hashtable = QUALITY_HASH_GRAY
|
||||
sumtable = QUALITY_SUM_GRAY
|
||||
except (MissingField, IndexError):
|
||||
# A coefficient is missing, so don't compute JPEG quality
|
||||
return
|
||||
|
||||
# Find the JPEG quality
|
||||
for index in xrange(100):
|
||||
if (hashval >= hashtable[index]) or (sumcoeff >= sumtable[index]):
|
||||
quality = "%s%%" % (index + 1)
|
||||
if (hashval > hashtable[index]) or (sumcoeff > sumtable[index]):
|
||||
quality += " " + _("(approximate)")
|
||||
self.comment = "JPEG quality: %s" % quality
|
||||
return
|
||||
|
||||
@fault_tolerant
|
||||
def extractAPP0(self, app0):
|
||||
self.format_version = u"JFIF %u.%02u" \
|
||||
% (app0["ver_maj"].value, app0["ver_min"].value)
|
||||
if "y_density" in app0:
|
||||
self.width_dpi = app0["x_density"].value
|
||||
self.height_dpi = app0["y_density"].value
|
||||
|
||||
@fault_tolerant
|
||||
def processIfdEntry(self, ifd, entry):
|
||||
# Skip unknown tags
|
||||
tag = entry["tag"].value
|
||||
if tag not in self.EXIF_KEY:
|
||||
return
|
||||
key = self.EXIF_KEY[tag]
|
||||
if key in ("width", "height") and self.has(key):
|
||||
# EXIF "valid size" are sometimes not updated when the image is scaled
|
||||
# so we just ignore it
|
||||
return
|
||||
|
||||
# Read value
|
||||
if "value" in entry:
|
||||
value = entry["value"].value
|
||||
else:
|
||||
value = ifd["value_%s" % entry.name].value
|
||||
|
||||
# Convert value to string
|
||||
if tag == ExifEntry.TAG_ORIENTATION:
|
||||
value = self.orientation_name.get(value, value)
|
||||
elif tag == ExifEntry.TAG_EXPOSURE:
|
||||
if not value:
|
||||
return
|
||||
if isinstance(value, float):
|
||||
value = (value, u"1/%g" % (1/value))
|
||||
elif entry["type"].value in (ExifEntry.TYPE_RATIONAL, ExifEntry.TYPE_SIGNED_RATIONAL):
|
||||
value = (value, u"%.3g" % value)
|
||||
|
||||
# Store information
|
||||
setattr(self, key, value)
|
||||
|
||||
@fault_tolerant
|
||||
def readGPS(self, ifd):
|
||||
# Read latitude and longitude
|
||||
latitude_ref = None
|
||||
longitude_ref = None
|
||||
latitude = None
|
||||
longitude = None
|
||||
altitude_ref = 1
|
||||
altitude = None
|
||||
timestamp = None
|
||||
datestamp = None
|
||||
for entry in ifd.array("entry"):
|
||||
tag = entry["tag"].value
|
||||
if tag == ExifEntry.TAG_GPS_LATITUDE_REF:
|
||||
if entry["value"].value == "N":
|
||||
latitude_ref = 1
|
||||
else:
|
||||
latitude_ref = -1
|
||||
elif tag == ExifEntry.TAG_GPS_LONGITUDE_REF:
|
||||
if entry["value"].value == "E":
|
||||
longitude_ref = 1
|
||||
else:
|
||||
longitude_ref = -1
|
||||
elif tag == ExifEntry.TAG_GPS_ALTITUDE_REF:
|
||||
if entry["value"].value == 1:
|
||||
altitude_ref = -1
|
||||
else:
|
||||
altitude_ref = 1
|
||||
elif tag == ExifEntry.TAG_GPS_LATITUDE:
|
||||
latitude = [ifd["value_%s[%u]" % (entry.name, index)].value for index in xrange(3)]
|
||||
elif tag == ExifEntry.TAG_GPS_LONGITUDE:
|
||||
longitude = [ifd["value_%s[%u]" % (entry.name, index)].value for index in xrange(3)]
|
||||
elif tag == ExifEntry.TAG_GPS_ALTITUDE:
|
||||
altitude = ifd["value_%s" % entry.name].value
|
||||
elif tag == ExifEntry.TAG_GPS_DATESTAMP:
|
||||
datestamp = ifd["value_%s" % entry.name].value
|
||||
elif tag == ExifEntry.TAG_GPS_TIMESTAMP:
|
||||
items = [ifd["value_%s[%u]" % (entry.name, index)].value for index in xrange(3)]
|
||||
items = map(int, items)
|
||||
items = map(str, items)
|
||||
timestamp = ":".join(items)
|
||||
if latitude_ref and latitude:
|
||||
value = deg2float(*latitude)
|
||||
if latitude_ref < 0:
|
||||
value = -value
|
||||
self.latitude = value
|
||||
if longitude and longitude_ref:
|
||||
value = deg2float(*longitude)
|
||||
if longitude_ref < 0:
|
||||
value = -value
|
||||
self.longitude = value
|
||||
if altitude:
|
||||
value = altitude
|
||||
if altitude_ref < 0:
|
||||
value = -value
|
||||
self.altitude = value
|
||||
if datestamp:
|
||||
if timestamp:
|
||||
datestamp += " " + timestamp
|
||||
self.creation_date = datestamp
|
||||
|
||||
def parseIPTC(self, iptc):
|
||||
datestr = hourstr = None
|
||||
for field in iptc:
|
||||
# Skip incomplete field
|
||||
if "tag" not in field or "content" not in field:
|
||||
continue
|
||||
|
||||
# Get value
|
||||
value = field["content"].value
|
||||
if isinstance(value, (str, unicode)):
|
||||
value = value.replace("\r", " ")
|
||||
value = value.replace("\n", " ")
|
||||
|
||||
# Skip unknown tag
|
||||
tag = field["tag"].value
|
||||
if tag == 55:
|
||||
datestr = value
|
||||
continue
|
||||
if tag == 60:
|
||||
hourstr = value
|
||||
continue
|
||||
if tag not in self.IPTC_KEY:
|
||||
if tag != 0:
|
||||
self.warning("Skip IPTC key %s: %s" % (
|
||||
field["tag"].display, makeUnicode(value)))
|
||||
continue
|
||||
setattr(self, self.IPTC_KEY[tag], value)
|
||||
if datestr and hourstr:
|
||||
try:
|
||||
year = int(datestr[0:4])
|
||||
month = int(datestr[4:6])
|
||||
day = int(datestr[6:8])
|
||||
hour = int(hourstr[0:2])
|
||||
min = int(hourstr[2:4])
|
||||
sec = int(hourstr[4:6])
|
||||
self.creation_date = datetime(year, month, day, hour, min, sec)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
registerExtractor(JpegFile, JpegMetadata)
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from hachoir_core.compatibility import any, sorted
|
||||
from hachoir_core.endian import endian_name
|
||||
from hachoir_core.tools import makePrintable, makeUnicode
|
||||
from hachoir_core.dict import Dict
|
||||
from hachoir_core.error import error, HACHOIR_ERRORS
|
||||
from hachoir_core.i18n import _
|
||||
from hachoir_core.log import Logger
|
||||
from hachoir_metadata.metadata_item import (
|
||||
MIN_PRIORITY, MAX_PRIORITY, QUALITY_NORMAL)
|
||||
from hachoir_metadata.register import registerAllItems
|
||||
|
||||
extractors = {}
|
||||
|
||||
class Metadata(Logger):
|
||||
header = u"Metadata"
|
||||
|
||||
def __init__(self, parent, quality=QUALITY_NORMAL):
|
||||
assert isinstance(self.header, unicode)
|
||||
|
||||
# Limit to 0.0 .. 1.0
|
||||
if parent:
|
||||
quality = parent.quality
|
||||
else:
|
||||
quality = min(max(0.0, quality), 1.0)
|
||||
|
||||
object.__init__(self)
|
||||
object.__setattr__(self, "_Metadata__data", {})
|
||||
object.__setattr__(self, "quality", quality)
|
||||
header = self.__class__.header
|
||||
object.__setattr__(self, "_Metadata__header", header)
|
||||
|
||||
registerAllItems(self)
|
||||
|
||||
def _logger(self):
|
||||
pass
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
"""
|
||||
Add a new value to data with name 'key'. Skip duplicates.
|
||||
"""
|
||||
# Invalid key?
|
||||
if key not in self.__data:
|
||||
raise KeyError(_("%s has no metadata '%s'") % (self.__class__.__name__, key))
|
||||
|
||||
# Skip duplicates
|
||||
self.__data[key].add(value)
|
||||
|
||||
def setHeader(self, text):
|
||||
object.__setattr__(self, "header", text)
|
||||
|
||||
def getItems(self, key):
|
||||
try:
|
||||
return self.__data[key]
|
||||
except LookupError:
|
||||
raise ValueError("Metadata has no value '%s'" % key)
|
||||
|
||||
def getItem(self, key, index):
|
||||
try:
|
||||
return self.getItems(key)[index]
|
||||
except (LookupError, ValueError):
|
||||
return None
|
||||
|
||||
def has(self, key):
|
||||
return 1 <= len(self.getItems(key))
|
||||
|
||||
def get(self, key, default=None, index=0):
|
||||
"""
|
||||
Read first value of tag with name 'key'.
|
||||
|
||||
>>> from datetime import timedelta
|
||||
>>> a = RootMetadata()
|
||||
>>> a.duration = timedelta(seconds=2300)
|
||||
>>> a.get('duration')
|
||||
datetime.timedelta(0, 2300)
|
||||
>>> a.get('author', u'Anonymous')
|
||||
u'Anonymous'
|
||||
"""
|
||||
item = self.getItem(key, index)
|
||||
if item is None:
|
||||
if default is None:
|
||||
raise ValueError("Metadata has no value '%s' (index %s)" % (key, index))
|
||||
else:
|
||||
return default
|
||||
return item.value
|
||||
|
||||
def getValues(self, key):
|
||||
try:
|
||||
data = self.__data[key]
|
||||
except LookupError:
|
||||
raise ValueError("Metadata has no value '%s'" % key)
|
||||
return [ item.value for item in data ]
|
||||
|
||||
def getText(self, key, default=None, index=0):
|
||||
"""
|
||||
Read first value, as unicode string, of tag with name 'key'.
|
||||
|
||||
>>> from datetime import timedelta
|
||||
>>> a = RootMetadata()
|
||||
>>> a.duration = timedelta(seconds=2300)
|
||||
>>> a.getText('duration')
|
||||
u'38 min 20 sec'
|
||||
>>> a.getText('titre', u'Unknown')
|
||||
u'Unknown'
|
||||
"""
|
||||
item = self.getItem(key, index)
|
||||
if item is not None:
|
||||
return item.text
|
||||
else:
|
||||
return default
|
||||
|
||||
def register(self, data):
|
||||
assert data.key not in self.__data
|
||||
data.metadata = self
|
||||
self.__data[data.key] = data
|
||||
|
||||
def __iter__(self):
|
||||
return self.__data.itervalues()
|
||||
|
||||
def __str__(self):
|
||||
r"""
|
||||
Create a multi-line ASCII string (end of line is "\n") which
|
||||
represents all datas.
|
||||
|
||||
>>> a = RootMetadata()
|
||||
>>> a.author = "haypo"
|
||||
>>> a.copyright = unicode("© Hachoir", "UTF-8")
|
||||
>>> print a
|
||||
Metadata:
|
||||
- Author: haypo
|
||||
- Copyright: \xa9 Hachoir
|
||||
|
||||
@see __unicode__() and exportPlaintext()
|
||||
"""
|
||||
text = self.exportPlaintext()
|
||||
return "\n".join( makePrintable(line, "ASCII") for line in text )
|
||||
|
||||
def __unicode__(self):
|
||||
r"""
|
||||
Create a multi-line Unicode string (end of line is "\n") which
|
||||
represents all datas.
|
||||
|
||||
>>> a = RootMetadata()
|
||||
>>> a.copyright = unicode("© Hachoir", "UTF-8")
|
||||
>>> print repr(unicode(a))
|
||||
u'Metadata:\n- Copyright: \xa9 Hachoir'
|
||||
|
||||
@see __str__() and exportPlaintext()
|
||||
"""
|
||||
return "\n".join(self.exportPlaintext())
|
||||
|
||||
def exportPlaintext(self, priority=None, human=True, line_prefix=u"- ", title=None):
|
||||
r"""
|
||||
Convert metadata to multi-line Unicode string and skip datas
|
||||
with priority lower than specified priority.
|
||||
|
||||
Default priority is Metadata.MAX_PRIORITY. If human flag is True, data
|
||||
key are translated to better human name (eg. "bit_rate" becomes
|
||||
"Bit rate") which may be translated using gettext.
|
||||
|
||||
If priority is too small, metadata are empty and so None is returned.
|
||||
|
||||
>>> print RootMetadata().exportPlaintext()
|
||||
None
|
||||
>>> meta = RootMetadata()
|
||||
>>> meta.copyright = unicode("© Hachoir", "UTF-8")
|
||||
>>> print repr(meta.exportPlaintext())
|
||||
[u'Metadata:', u'- Copyright: \xa9 Hachoir']
|
||||
|
||||
@see __str__() and __unicode__()
|
||||
"""
|
||||
if priority is not None:
|
||||
priority = max(priority, MIN_PRIORITY)
|
||||
priority = min(priority, MAX_PRIORITY)
|
||||
else:
|
||||
priority = MAX_PRIORITY
|
||||
if not title:
|
||||
title = self.header
|
||||
text = ["%s:" % title]
|
||||
for data in sorted(self):
|
||||
if priority < data.priority:
|
||||
break
|
||||
if not data.values:
|
||||
continue
|
||||
if human:
|
||||
title = data.description
|
||||
else:
|
||||
title = data.key
|
||||
for item in data.values:
|
||||
if human:
|
||||
value = item.text
|
||||
else:
|
||||
value = makeUnicode(item.value)
|
||||
text.append("%s%s: %s" % (line_prefix, title, value))
|
||||
if 1 < len(text):
|
||||
return text
|
||||
else:
|
||||
return None
|
||||
|
||||
def __nonzero__(self):
|
||||
return any(item for item in self.__data.itervalues())
|
||||
|
||||
class RootMetadata(Metadata):
|
||||
def __init__(self, quality=QUALITY_NORMAL):
|
||||
Metadata.__init__(self, None, quality)
|
||||
|
||||
class MultipleMetadata(RootMetadata):
|
||||
header = _("Common")
|
||||
def __init__(self, quality=QUALITY_NORMAL):
|
||||
RootMetadata.__init__(self, quality)
|
||||
object.__setattr__(self, "_MultipleMetadata__groups", Dict())
|
||||
object.__setattr__(self, "_MultipleMetadata__key_counter", {})
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.__groups
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.__groups[key]
|
||||
|
||||
def iterGroups(self):
|
||||
return self.__groups.itervalues()
|
||||
|
||||
def __nonzero__(self):
|
||||
if RootMetadata.__nonzero__(self):
|
||||
return True
|
||||
return any(bool(group) for group in self.__groups)
|
||||
|
||||
def addGroup(self, key, metadata, header=None):
|
||||
"""
|
||||
Add a new group (metadata of a sub-document).
|
||||
|
||||
Returns False if the group is skipped, True if it has been added.
|
||||
"""
|
||||
if not metadata:
|
||||
self.warning("Skip empty group %s" % key)
|
||||
return False
|
||||
if key.endswith("[]"):
|
||||
key = key[:-2]
|
||||
if key in self.__key_counter:
|
||||
self.__key_counter[key] += 1
|
||||
else:
|
||||
self.__key_counter[key] = 1
|
||||
key += "[%u]" % self.__key_counter[key]
|
||||
if header:
|
||||
metadata.setHeader(header)
|
||||
self.__groups.append(key, metadata)
|
||||
return True
|
||||
|
||||
def exportPlaintext(self, priority=None, human=True, line_prefix=u"- "):
|
||||
common = Metadata.exportPlaintext(self, priority, human, line_prefix)
|
||||
if common:
|
||||
text = common
|
||||
else:
|
||||
text = []
|
||||
for key, metadata in self.__groups.iteritems():
|
||||
if not human:
|
||||
title = key
|
||||
else:
|
||||
title = None
|
||||
value = metadata.exportPlaintext(priority, human, line_prefix, title=title)
|
||||
if value:
|
||||
text.extend(value)
|
||||
if len(text):
|
||||
return text
|
||||
else:
|
||||
return None
|
||||
|
||||
def registerExtractor(parser, extractor):
|
||||
assert parser not in extractors
|
||||
assert issubclass(extractor, RootMetadata)
|
||||
extractors[parser] = extractor
|
||||
|
||||
def extractMetadata(parser, quality=QUALITY_NORMAL):
|
||||
"""
|
||||
Create a Metadata class from a parser. Returns None if no metadata
|
||||
extractor does exist for the parser class.
|
||||
"""
|
||||
try:
|
||||
extractor = extractors[parser.__class__]
|
||||
except KeyError:
|
||||
return None
|
||||
metadata = extractor(quality)
|
||||
try:
|
||||
metadata.extract(parser)
|
||||
except HACHOIR_ERRORS, err:
|
||||
error("Error during metadata extraction: %s" % unicode(err))
|
||||
if metadata:
|
||||
metadata.mime_type = parser.mime_type
|
||||
metadata.endian = endian_name[parser.endian]
|
||||
return metadata
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
from hachoir_core.tools import makeUnicode, normalizeNewline
|
||||
from hachoir_core.error import HACHOIR_ERRORS
|
||||
from hachoir_metadata import config
|
||||
from hachoir_metadata.setter import normalizeString
|
||||
|
||||
MIN_PRIORITY = 100
|
||||
MAX_PRIORITY = 999
|
||||
|
||||
QUALITY_FASTEST = 0.0
|
||||
QUALITY_FAST = 0.25
|
||||
QUALITY_NORMAL = 0.5
|
||||
QUALITY_GOOD = 0.75
|
||||
QUALITY_BEST = 1.0
|
||||
|
||||
class DataValue:
|
||||
def __init__(self, value, text):
|
||||
self.value = value
|
||||
self.text = text
|
||||
|
||||
class Data:
|
||||
def __init__(self, key, priority, description,
|
||||
text_handler=None, type=None, filter=None, conversion=None):
|
||||
"""
|
||||
handler is only used if value is not string nor unicode, prototype:
|
||||
def handler(value) -> str/unicode
|
||||
"""
|
||||
assert MIN_PRIORITY <= priority <= MAX_PRIORITY
|
||||
assert isinstance(description, unicode)
|
||||
self.metadata = None
|
||||
self.key = key
|
||||
self.description = description
|
||||
self.values = []
|
||||
if type and not isinstance(type, (tuple, list)):
|
||||
type = (type,)
|
||||
self.type = type
|
||||
self.text_handler = text_handler
|
||||
self.filter = filter
|
||||
self.priority = priority
|
||||
self.conversion = conversion
|
||||
|
||||
def _createItem(self, value, text=None):
|
||||
if text is None:
|
||||
if isinstance(value, unicode):
|
||||
text = value
|
||||
elif self.text_handler:
|
||||
text = self.text_handler(value)
|
||||
assert isinstance(text, unicode)
|
||||
else:
|
||||
text = makeUnicode(value)
|
||||
return DataValue(value, text)
|
||||
|
||||
def add(self, value):
|
||||
if isinstance(value, tuple):
|
||||
if len(value) != 2:
|
||||
raise ValueError("Data.add() only accept tuple of 2 elements: (value,text)")
|
||||
value, text = value
|
||||
else:
|
||||
text = None
|
||||
|
||||
# Skip value 'None'
|
||||
if value is None:
|
||||
return
|
||||
|
||||
if isinstance(value, (str, unicode)):
|
||||
value = normalizeString(value)
|
||||
if not value:
|
||||
return
|
||||
|
||||
# Convert string to Unicode string using charset ISO-8859-1
|
||||
if self.conversion:
|
||||
try:
|
||||
new_value = self.conversion(self.metadata, self.key, value)
|
||||
except HACHOIR_ERRORS, err:
|
||||
self.metadata.warning("Error during conversion of %r value: %s" % (
|
||||
self.key, err))
|
||||
return
|
||||
if new_value is None:
|
||||
dest_types = " or ".join(str(item.__name__) for item in self.type)
|
||||
self.metadata.warning("Unable to convert %s=%r (%s) to %s" % (
|
||||
self.key, value, type(value).__name__, dest_types))
|
||||
return
|
||||
if isinstance(new_value, tuple):
|
||||
if text:
|
||||
value = new_value[0]
|
||||
else:
|
||||
value, text = new_value
|
||||
else:
|
||||
value = new_value
|
||||
elif isinstance(value, str):
|
||||
value = unicode(value, "ISO-8859-1")
|
||||
|
||||
if self.type and not isinstance(value, self.type):
|
||||
dest_types = " or ".join(str(item.__name__) for item in self.type)
|
||||
self.metadata.warning("Key %r: value %r type (%s) is not %s" % (
|
||||
self.key, value, type(value).__name__, dest_types))
|
||||
return
|
||||
|
||||
# Skip empty strings
|
||||
if isinstance(value, unicode):
|
||||
value = normalizeNewline(value)
|
||||
if config.MAX_STR_LENGTH \
|
||||
and config.MAX_STR_LENGTH < len(value):
|
||||
value = value[:config.MAX_STR_LENGTH] + "(...)"
|
||||
|
||||
# Skip duplicates
|
||||
if value in self:
|
||||
return
|
||||
|
||||
# Use filter
|
||||
if self.filter and not self.filter(value):
|
||||
self.metadata.warning("Skip value %s=%r (filter)" % (self.key, value))
|
||||
return
|
||||
|
||||
# For string, if you have "verlongtext" and "verylo",
|
||||
# keep the longer value
|
||||
if isinstance(value, unicode):
|
||||
for index, item in enumerate(self.values):
|
||||
item = item.value
|
||||
if not isinstance(item, unicode):
|
||||
continue
|
||||
if value.startswith(item):
|
||||
# Find longer value, replace the old one
|
||||
self.values[index] = self._createItem(value, text)
|
||||
return
|
||||
if item.startswith(value):
|
||||
# Find truncated value, skip it
|
||||
return
|
||||
|
||||
# Add new value
|
||||
self.values.append(self._createItem(value, text))
|
||||
|
||||
def __len__(self):
|
||||
return len(self.values)
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.values[index]
|
||||
|
||||
def __contains__(self, value):
|
||||
for item in self.values:
|
||||
if value == item.value:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.priority, other.priority)
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
from hachoir_metadata.metadata import RootMetadata, registerExtractor
|
||||
from hachoir_metadata.safe import fault_tolerant
|
||||
from hachoir_parser.container import SwfFile
|
||||
from hachoir_parser.misc import TorrentFile, TrueTypeFontFile, OLE2_File, PcfFile
|
||||
from hachoir_core.field import isString
|
||||
from hachoir_core.error import warning
|
||||
from hachoir_parser import guessParser
|
||||
from hachoir_metadata.setter import normalizeString
|
||||
|
||||
class TorrentMetadata(RootMetadata):
|
||||
KEY_TO_ATTR = {
|
||||
u"announce": "url",
|
||||
u"comment": "comment",
|
||||
u"creation_date": "creation_date",
|
||||
}
|
||||
INFO_TO_ATTR = {
|
||||
u"length": "file_size",
|
||||
u"name": "filename",
|
||||
}
|
||||
|
||||
def extract(self, torrent):
|
||||
for field in torrent[0]:
|
||||
self.processRoot(field)
|
||||
|
||||
@fault_tolerant
|
||||
def processRoot(self, field):
|
||||
if field.name in self.KEY_TO_ATTR:
|
||||
key = self.KEY_TO_ATTR[field.name]
|
||||
value = field.value
|
||||
setattr(self, key, value)
|
||||
elif field.name == "info" and "value" in field:
|
||||
for field in field["value"]:
|
||||
self.processInfo(field)
|
||||
|
||||
@fault_tolerant
|
||||
def processInfo(self, field):
|
||||
if field.name in self.INFO_TO_ATTR:
|
||||
key = self.INFO_TO_ATTR[field.name]
|
||||
value = field.value
|
||||
setattr(self, key, value)
|
||||
elif field.name == "piece_length":
|
||||
self.comment = "Piece length: %s" % field.display
|
||||
|
||||
class TTF_Metadata(RootMetadata):
|
||||
NAMEID_TO_ATTR = {
|
||||
0: "copyright", # Copyright notice
|
||||
3: "title", # Unique font identifier
|
||||
5: "version", # Version string
|
||||
8: "author", # Manufacturer name
|
||||
11: "url", # URL Vendor
|
||||
14: "copyright", # License info URL
|
||||
}
|
||||
|
||||
def extract(self, ttf):
|
||||
if "header" in ttf:
|
||||
self.extractHeader(ttf["header"])
|
||||
if "names" in ttf:
|
||||
self.extractNames(ttf["names"])
|
||||
|
||||
@fault_tolerant
|
||||
def extractHeader(self, header):
|
||||
self.creation_date = header["created"].value
|
||||
self.last_modification = header["modified"].value
|
||||
self.comment = u"Smallest readable size in pixels: %s pixels" % header["lowest"].value
|
||||
self.comment = u"Font direction: %s" % header["font_dir"].display
|
||||
|
||||
@fault_tolerant
|
||||
def extractNames(self, names):
|
||||
offset = names["offset"].value
|
||||
for header in names.array("header"):
|
||||
key = header["nameID"].value
|
||||
foffset = offset + header["offset"].value
|
||||
field = names.getFieldByAddress(foffset*8)
|
||||
if not field or not isString(field):
|
||||
continue
|
||||
value = field.value
|
||||
if key not in self.NAMEID_TO_ATTR:
|
||||
continue
|
||||
key = self.NAMEID_TO_ATTR[key]
|
||||
if key == "version" and value.startswith(u"Version "):
|
||||
# "Version 1.2" => "1.2"
|
||||
value = value[8:]
|
||||
setattr(self, key, value)
|
||||
|
||||
class OLE2_Metadata(RootMetadata):
|
||||
SUMMARY_ID_TO_ATTR = {
|
||||
2: "title", # Title
|
||||
3: "title", # Subject
|
||||
4: "author",
|
||||
6: "comment",
|
||||
8: "author", # Last saved by
|
||||
12: "creation_date",
|
||||
13: "last_modification",
|
||||
14: "nb_page",
|
||||
18: "producer",
|
||||
}
|
||||
IGNORE_SUMMARY = set((
|
||||
1, # Code page
|
||||
))
|
||||
|
||||
DOC_SUMMARY_ID_TO_ATTR = {
|
||||
3: "title", # Subject
|
||||
14: "author", # Manager
|
||||
}
|
||||
IGNORE_DOC_SUMMARY = set((
|
||||
1, # Code page
|
||||
))
|
||||
|
||||
def extract(self, ole2):
|
||||
self._extract(ole2)
|
||||
|
||||
def _extract(self, fieldset, main_document=True):
|
||||
if main_document:
|
||||
# _feedAll() is needed to make sure that we get all root[*] fragments
|
||||
fieldset._feedAll()
|
||||
if "root[0]" in fieldset:
|
||||
self.useRoot(fieldset["root[0]"])
|
||||
doc_summary = self.getField(fieldset, main_document, "doc_summary[0]")
|
||||
if doc_summary:
|
||||
self.useSummary(doc_summary, True)
|
||||
word_doc = self.getField(fieldset, main_document, "word_doc[0]")
|
||||
if word_doc:
|
||||
self.useWordDocument(word_doc)
|
||||
summary = self.getField(fieldset, main_document, "summary[0]")
|
||||
if summary:
|
||||
self.useSummary(summary, False)
|
||||
|
||||
@fault_tolerant
|
||||
def useRoot(self, root):
|
||||
stream = root.getSubIStream()
|
||||
ministream = guessParser(stream)
|
||||
if not ministream:
|
||||
warning("Unable to create the OLE2 mini stream parser!")
|
||||
return
|
||||
self._extract(ministream, main_document=False)
|
||||
|
||||
def getField(self, fieldset, main_document, name):
|
||||
if name not in fieldset:
|
||||
return None
|
||||
# _feedAll() is needed to make sure that we get all fragments
|
||||
# eg. summary[0], summary[1], ..., summary[n]
|
||||
fieldset._feedAll()
|
||||
field = fieldset[name]
|
||||
if main_document:
|
||||
stream = field.getSubIStream()
|
||||
field = guessParser(stream)
|
||||
if not field:
|
||||
warning("Unable to create the OLE2 parser for %s!" % name)
|
||||
return None
|
||||
return field
|
||||
|
||||
@fault_tolerant
|
||||
def useSummary(self, summary, is_doc_summary):
|
||||
if "os" in summary:
|
||||
self.os = summary["os"].display
|
||||
if "section[0]" not in summary:
|
||||
return
|
||||
summary = summary["section[0]"]
|
||||
for property in summary.array("property_index"):
|
||||
self.useProperty(summary, property, is_doc_summary)
|
||||
|
||||
@fault_tolerant
|
||||
def useWordDocument(self, doc):
|
||||
self.comment = "Encrypted: %s" % doc["fEncrypted"].value
|
||||
|
||||
@fault_tolerant
|
||||
def useProperty(self, summary, property, is_doc_summary):
|
||||
field = summary.getFieldByAddress(property["offset"].value*8)
|
||||
if not field \
|
||||
or "value" not in field:
|
||||
return
|
||||
field = field["value"]
|
||||
if not field.hasValue():
|
||||
return
|
||||
|
||||
# Get value
|
||||
value = field.value
|
||||
if isinstance(value, (str, unicode)):
|
||||
value = normalizeString(value)
|
||||
if not value:
|
||||
return
|
||||
|
||||
# Get property identifier
|
||||
prop_id = property["id"].value
|
||||
if is_doc_summary:
|
||||
id_to_attr = self.DOC_SUMMARY_ID_TO_ATTR
|
||||
ignore = self.IGNORE_DOC_SUMMARY
|
||||
else:
|
||||
id_to_attr = self.SUMMARY_ID_TO_ATTR
|
||||
ignore = self.IGNORE_SUMMARY
|
||||
if prop_id in ignore:
|
||||
return
|
||||
|
||||
# Get Hachoir metadata key
|
||||
try:
|
||||
key = id_to_attr[prop_id]
|
||||
use_prefix = False
|
||||
except LookupError:
|
||||
key = "comment"
|
||||
use_prefix = True
|
||||
if use_prefix:
|
||||
prefix = property["id"].display
|
||||
if (prefix in ("TotalEditingTime", "LastPrinted")) \
|
||||
and (not field):
|
||||
# Ignore null time delta
|
||||
return
|
||||
value = "%s: %s" % (prefix, value)
|
||||
else:
|
||||
if (key == "last_modification") and (not field):
|
||||
# Ignore null timestamp
|
||||
return
|
||||
setattr(self, key, value)
|
||||
|
||||
class PcfMetadata(RootMetadata):
|
||||
PROP_TO_KEY = {
|
||||
'CHARSET_REGISTRY': 'charset',
|
||||
'COPYRIGHT': 'copyright',
|
||||
'WEIGHT_NAME': 'font_weight',
|
||||
'FOUNDRY': 'author',
|
||||
'FONT': 'title',
|
||||
'_XMBDFED_INFO': 'producer',
|
||||
}
|
||||
|
||||
def extract(self, pcf):
|
||||
if "properties" in pcf:
|
||||
self.useProperties(pcf["properties"])
|
||||
|
||||
def useProperties(self, properties):
|
||||
last = properties["total_str_length"]
|
||||
offset0 = last.address + last.size
|
||||
for index in properties.array("property"):
|
||||
# Search name and value
|
||||
value = properties.getFieldByAddress(offset0+index["value_offset"].value*8)
|
||||
if not value:
|
||||
continue
|
||||
value = value.value
|
||||
if not value:
|
||||
continue
|
||||
name = properties.getFieldByAddress(offset0+index["name_offset"].value*8)
|
||||
if not name:
|
||||
continue
|
||||
name = name.value
|
||||
if name not in self.PROP_TO_KEY:
|
||||
warning("Skip %s=%r" % (name, value))
|
||||
continue
|
||||
key = self.PROP_TO_KEY[name]
|
||||
setattr(self, key, value)
|
||||
|
||||
class SwfMetadata(RootMetadata):
|
||||
def extract(self, swf):
|
||||
self.height = swf["rect/ymax"].value # twips
|
||||
self.width = swf["rect/xmax"].value # twips
|
||||
self.format_version = "flash version %s" % swf["version"].value
|
||||
self.frame_rate = swf["frame_rate"].value
|
||||
self.comment = "Frame count: %s" % swf["frame_count"].value
|
||||
|
||||
registerExtractor(TorrentFile, TorrentMetadata)
|
||||
registerExtractor(TrueTypeFontFile, TTF_Metadata)
|
||||
registerExtractor(OLE2_File, OLE2_Metadata)
|
||||
registerExtractor(PcfFile, PcfMetadata)
|
||||
registerExtractor(SwfFile, SwfMetadata)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
from hachoir_metadata.metadata import RootMetadata, registerExtractor
|
||||
from hachoir_parser.program import ExeFile
|
||||
from hachoir_metadata.safe import fault_tolerant, getValue
|
||||
|
||||
class ExeMetadata(RootMetadata):
|
||||
KEY_TO_ATTR = {
|
||||
u"ProductName": "title",
|
||||
u"LegalCopyright": "copyright",
|
||||
u"LegalTrademarks": "copyright",
|
||||
u"LegalTrademarks1": "copyright",
|
||||
u"LegalTrademarks2": "copyright",
|
||||
u"CompanyName": "author",
|
||||
u"BuildDate": "creation_date",
|
||||
u"FileDescription": "title",
|
||||
u"ProductVersion": "version",
|
||||
}
|
||||
SKIP_KEY = set((u"InternalName", u"OriginalFilename", u"FileVersion", u"BuildVersion"))
|
||||
|
||||
def extract(self, exe):
|
||||
if exe.isPE():
|
||||
self.extractPE(exe)
|
||||
elif exe.isNE():
|
||||
self.extractNE(exe)
|
||||
|
||||
def extractNE(self, exe):
|
||||
if "ne_header" in exe:
|
||||
self.useNE_Header(exe["ne_header"])
|
||||
if "info" in exe:
|
||||
self.useNEInfo(exe["info"])
|
||||
|
||||
@fault_tolerant
|
||||
def useNEInfo(self, info):
|
||||
for node in info.array("node"):
|
||||
if node["name"].value == "StringFileInfo":
|
||||
self.readVersionInfo(node["node[0]"])
|
||||
|
||||
def extractPE(self, exe):
|
||||
# Read information from headers
|
||||
if "pe_header" in exe:
|
||||
self.usePE_Header(exe["pe_header"])
|
||||
if "pe_opt_header" in exe:
|
||||
self.usePE_OptHeader(exe["pe_opt_header"])
|
||||
|
||||
# Use PE resource
|
||||
resource = exe.getResource()
|
||||
if resource and "version_info/node[0]" in resource:
|
||||
for node in resource.array("version_info/node[0]/node"):
|
||||
if getValue(node, "name") == "StringFileInfo" \
|
||||
and "node[0]" in node:
|
||||
self.readVersionInfo(node["node[0]"])
|
||||
|
||||
@fault_tolerant
|
||||
def useNE_Header(self, hdr):
|
||||
if hdr["is_dll"].value:
|
||||
self.format_version = u"New-style executable: Dynamic-link library (DLL)"
|
||||
elif hdr["is_win_app"].value:
|
||||
self.format_version = u"New-style executable: Windows 3.x application"
|
||||
else:
|
||||
self.format_version = u"New-style executable for Windows 3.x"
|
||||
|
||||
@fault_tolerant
|
||||
def usePE_Header(self, hdr):
|
||||
self.creation_date = hdr["creation_date"].value
|
||||
self.comment = "CPU: %s" % hdr["cpu"].display
|
||||
if hdr["is_dll"].value:
|
||||
self.format_version = u"Portable Executable: Dynamic-link library (DLL)"
|
||||
else:
|
||||
self.format_version = u"Portable Executable: Windows application"
|
||||
|
||||
@fault_tolerant
|
||||
def usePE_OptHeader(self, hdr):
|
||||
self.comment = "Subsystem: %s" % hdr["subsystem"].display
|
||||
|
||||
def readVersionInfo(self, info):
|
||||
values = {}
|
||||
for node in info.array("node"):
|
||||
if "value" not in node or "name" not in node:
|
||||
continue
|
||||
value = node["value"].value.strip(" \0")
|
||||
if not value:
|
||||
continue
|
||||
key = node["name"].value
|
||||
values[key] = value
|
||||
|
||||
if "ProductName" in values and "FileDescription" in values:
|
||||
# Make sure that FileDescription is set before ProductName
|
||||
# as title value
|
||||
self.title = values["FileDescription"]
|
||||
self.title = values["ProductName"]
|
||||
del values["FileDescription"]
|
||||
del values["ProductName"]
|
||||
|
||||
for key, value in values.iteritems():
|
||||
if key in self.KEY_TO_ATTR:
|
||||
setattr(self, self.KEY_TO_ATTR[key], value)
|
||||
elif key not in self.SKIP_KEY:
|
||||
self.comment = "%s=%s" % (key, value)
|
||||
|
||||
registerExtractor(ExeFile, ExeMetadata)
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,64 @@
|
||||
<ui version="4.0" >
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form" >
|
||||
<property name="geometry" >
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>441</width>
|
||||
<height>412</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle" >
|
||||
<string>hachoir-metadata</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout" >
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2" >
|
||||
<item>
|
||||
<widget class="QPushButton" name="open_button" >
|
||||
<property name="text" >
|
||||
<string>Open</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="files_combo" >
|
||||
<property name="sizePolicy" >
|
||||
<sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="metadata_table" >
|
||||
<property name="alternatingRowColors" >
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="showGrid" >
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="rowCount" >
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount" >
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="quit_button" >
|
||||
<property name="text" >
|
||||
<string>Quit</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,112 @@
|
||||
from hachoir_core.i18n import _
|
||||
from hachoir_core.tools import (
|
||||
humanDuration, humanBitRate,
|
||||
humanFrequency, humanBitSize, humanFilesize,
|
||||
humanDatetime)
|
||||
from hachoir_core.language import Language
|
||||
from hachoir_metadata.filter import Filter, NumberFilter, DATETIME_FILTER
|
||||
from datetime import date, datetime, timedelta
|
||||
from hachoir_metadata.formatter import (
|
||||
humanAudioChannel, humanFrameRate, humanComprRate, humanAltitude,
|
||||
humanPixelSize, humanDPI)
|
||||
from hachoir_metadata.setter import (
|
||||
setDatetime, setTrackNumber, setTrackTotal, setLanguage)
|
||||
from hachoir_metadata.metadata_item import Data
|
||||
|
||||
MIN_SAMPLE_RATE = 1000 # 1 kHz
|
||||
MAX_SAMPLE_RATE = 192000 # 192 kHz
|
||||
MAX_NB_CHANNEL = 8 # 8 channels
|
||||
MAX_WIDTH = 20000 # 20 000 pixels
|
||||
MAX_BIT_RATE = 500 * 1024 * 1024 # 500 Mbit/s
|
||||
MAX_HEIGHT = MAX_WIDTH
|
||||
MAX_DPI_WIDTH = 10000
|
||||
MAX_DPI_HEIGHT = MAX_DPI_WIDTH
|
||||
MAX_NB_COLOR = 2 ** 24 # 16 million of color
|
||||
MAX_BITS_PER_PIXEL = 256 # 256 bits/pixel
|
||||
MAX_FRAME_RATE = 150 # 150 frame/sec
|
||||
MAX_NB_PAGE = 20000
|
||||
MAX_COMPR_RATE = 1000.0
|
||||
MIN_COMPR_RATE = 0.001
|
||||
MAX_TRACK = 999
|
||||
|
||||
DURATION_FILTER = Filter(timedelta,
|
||||
timedelta(milliseconds=1),
|
||||
timedelta(days=365))
|
||||
|
||||
def registerAllItems(meta):
|
||||
meta.register(Data("title", 100, _("Title"), type=unicode))
|
||||
meta.register(Data("artist", 101, _("Artist"), type=unicode))
|
||||
meta.register(Data("author", 102, _("Author"), type=unicode))
|
||||
meta.register(Data("music_composer", 103, _("Music composer"), type=unicode))
|
||||
|
||||
meta.register(Data("album", 200, _("Album"), type=unicode))
|
||||
meta.register(Data("duration", 201, _("Duration"), # integer in milliseconde
|
||||
type=timedelta, text_handler=humanDuration, filter=DURATION_FILTER))
|
||||
meta.register(Data("nb_page", 202, _("Nb page"), filter=NumberFilter(1, MAX_NB_PAGE)))
|
||||
meta.register(Data("music_genre", 203, _("Music genre"), type=unicode))
|
||||
meta.register(Data("language", 204, _("Language"), conversion=setLanguage, type=Language))
|
||||
meta.register(Data("track_number", 205, _("Track number"), conversion=setTrackNumber,
|
||||
filter=NumberFilter(1, MAX_TRACK), type=(int, long)))
|
||||
meta.register(Data("track_total", 206, _("Track total"), conversion=setTrackTotal,
|
||||
filter=NumberFilter(1, MAX_TRACK), type=(int, long)))
|
||||
meta.register(Data("organization", 210, _("Organization"), type=unicode))
|
||||
meta.register(Data("version", 220, _("Version")))
|
||||
|
||||
|
||||
meta.register(Data("width", 301, _("Image width"), filter=NumberFilter(1, MAX_WIDTH), type=(int, long), text_handler=humanPixelSize))
|
||||
meta.register(Data("height", 302, _("Image height"), filter=NumberFilter(1, MAX_HEIGHT), type=(int, long), text_handler=humanPixelSize))
|
||||
meta.register(Data("nb_channel", 303, _("Channel"), text_handler=humanAudioChannel, filter=NumberFilter(1, MAX_NB_CHANNEL), type=(int, long)))
|
||||
meta.register(Data("sample_rate", 304, _("Sample rate"), text_handler=humanFrequency, filter=NumberFilter(MIN_SAMPLE_RATE, MAX_SAMPLE_RATE), type=(int, long, float)))
|
||||
meta.register(Data("bits_per_sample", 305, _("Bits/sample"), text_handler=humanBitSize, filter=NumberFilter(1, 64), type=(int, long)))
|
||||
meta.register(Data("image_orientation", 306, _("Image orientation")))
|
||||
meta.register(Data("nb_colors", 307, _("Number of colors"), filter=NumberFilter(1, MAX_NB_COLOR), type=(int, long)))
|
||||
meta.register(Data("bits_per_pixel", 308, _("Bits/pixel"), filter=NumberFilter(1, MAX_BITS_PER_PIXEL), type=(int, long)))
|
||||
meta.register(Data("filename", 309, _("File name"), type=unicode))
|
||||
meta.register(Data("file_size", 310, _("File size"), text_handler=humanFilesize, type=(int, long)))
|
||||
meta.register(Data("pixel_format", 311, _("Pixel format")))
|
||||
meta.register(Data("compr_size", 312, _("Compressed file size"), text_handler=humanFilesize, type=(int, long)))
|
||||
meta.register(Data("compr_rate", 313, _("Compression rate"), text_handler=humanComprRate, filter=NumberFilter(MIN_COMPR_RATE, MAX_COMPR_RATE), type=(int, long, float)))
|
||||
|
||||
meta.register(Data("width_dpi", 320, _("Image DPI width"), filter=NumberFilter(1, MAX_DPI_WIDTH), type=(int, long), text_handler=humanDPI))
|
||||
meta.register(Data("height_dpi", 321, _("Image DPI height"), filter=NumberFilter(1, MAX_DPI_HEIGHT), type=(int, long), text_handler=humanDPI))
|
||||
|
||||
meta.register(Data("file_attr", 400, _("File attributes")))
|
||||
meta.register(Data("file_type", 401, _("File type")))
|
||||
meta.register(Data("subtitle_author", 402, _("Subtitle author"), type=unicode))
|
||||
|
||||
meta.register(Data("creation_date", 500, _("Creation date"), text_handler=humanDatetime,
|
||||
filter=DATETIME_FILTER, type=(datetime, date), conversion=setDatetime))
|
||||
meta.register(Data("last_modification", 501, _("Last modification"), text_handler=humanDatetime,
|
||||
filter=DATETIME_FILTER, type=(datetime, date), conversion=setDatetime))
|
||||
meta.register(Data("latitude", 510, _("Latitude"), type=float))
|
||||
meta.register(Data("longitude", 511, _("Longitude"), type=float))
|
||||
meta.register(Data("altitude", 511, _("Altitude"), type=float, text_handler=humanAltitude))
|
||||
meta.register(Data("location", 530, _("Location"), type=unicode))
|
||||
meta.register(Data("city", 531, _("City"), type=unicode))
|
||||
meta.register(Data("country", 532, _("Country"), type=unicode))
|
||||
meta.register(Data("charset", 540, _("Charset"), type=unicode))
|
||||
meta.register(Data("font_weight", 550, _("Font weight")))
|
||||
|
||||
meta.register(Data("camera_aperture", 520, _("Camera aperture")))
|
||||
meta.register(Data("camera_focal", 521, _("Camera focal")))
|
||||
meta.register(Data("camera_exposure", 522, _("Camera exposure")))
|
||||
meta.register(Data("camera_brightness", 530, _("Camera brightness")))
|
||||
meta.register(Data("camera_model", 531, _("Camera model"), type=unicode))
|
||||
meta.register(Data("camera_manufacturer", 532, _("Camera manufacturer"), type=unicode))
|
||||
|
||||
meta.register(Data("compression", 600, _("Compression")))
|
||||
meta.register(Data("copyright", 601, _("Copyright"), type=unicode))
|
||||
meta.register(Data("url", 602, _("URL"), type=unicode))
|
||||
meta.register(Data("frame_rate", 603, _("Frame rate"), text_handler=humanFrameRate,
|
||||
filter=NumberFilter(1, MAX_FRAME_RATE), type=(int, long, float)))
|
||||
meta.register(Data("bit_rate", 604, _("Bit rate"), text_handler=humanBitRate,
|
||||
filter=NumberFilter(1, MAX_BIT_RATE), type=(int, long, float)))
|
||||
meta.register(Data("aspect_ratio", 604, _("Aspect ratio"), type=(int, long, float)))
|
||||
|
||||
meta.register(Data("os", 900, _("OS"), type=unicode))
|
||||
meta.register(Data("producer", 901, _("Producer"), type=unicode))
|
||||
meta.register(Data("comment", 902, _("Comment"), type=unicode))
|
||||
meta.register(Data("format_version", 950, _("Format version"), type=unicode))
|
||||
meta.register(Data("mime_type", 951, _("MIME type"), type=unicode))
|
||||
meta.register(Data("endian", 952, _("Endianness"), type=unicode))
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
Extract metadata from RIFF file format: AVI video and WAV sound.
|
||||
"""
|
||||
|
||||
from hachoir_metadata.metadata import Metadata, MultipleMetadata, registerExtractor
|
||||
from hachoir_metadata.safe import fault_tolerant, getValue
|
||||
from hachoir_parser.container.riff import RiffFile
|
||||
from hachoir_parser.video.fourcc import UNCOMPRESSED_AUDIO
|
||||
from hachoir_core.tools import humanFilesize, makeUnicode, timedelta2seconds
|
||||
from hachoir_core.i18n import _
|
||||
from hachoir_metadata.audio import computeComprRate as computeAudioComprRate
|
||||
from datetime import timedelta
|
||||
|
||||
class RiffMetadata(MultipleMetadata):
|
||||
TAG_TO_KEY = {
|
||||
"INAM": "title",
|
||||
"IART": "artist",
|
||||
"ICMT": "comment",
|
||||
"ICOP": "copyright",
|
||||
"IENG": "author", # (engineer)
|
||||
"ISFT": "producer",
|
||||
"ICRD": "creation_date",
|
||||
"IDIT": "creation_date",
|
||||
}
|
||||
|
||||
def extract(self, riff):
|
||||
type = riff["type"].value
|
||||
if type == "WAVE":
|
||||
self.extractWAVE(riff)
|
||||
size = getValue(riff, "audio_data/size")
|
||||
if size:
|
||||
computeAudioComprRate(self, size*8)
|
||||
elif type == "AVI ":
|
||||
if "headers" in riff:
|
||||
self.extractAVI(riff["headers"])
|
||||
self.extractInfo(riff["headers"])
|
||||
elif type == "ACON":
|
||||
self.extractAnim(riff)
|
||||
if "info" in riff:
|
||||
self.extractInfo(riff["info"])
|
||||
|
||||
def processChunk(self, chunk):
|
||||
if "text" not in chunk:
|
||||
return
|
||||
value = chunk["text"].value
|
||||
tag = chunk["tag"].value
|
||||
if tag not in self.TAG_TO_KEY:
|
||||
self.warning("Skip RIFF metadata %s: %s" % (tag, value))
|
||||
return
|
||||
key = self.TAG_TO_KEY[tag]
|
||||
setattr(self, key, value)
|
||||
|
||||
@fault_tolerant
|
||||
def extractWAVE(self, wav):
|
||||
format = wav["format"]
|
||||
|
||||
# Number of channel, bits/sample, sample rate
|
||||
self.nb_channel = format["nb_channel"].value
|
||||
self.bits_per_sample = format["bit_per_sample"].value
|
||||
self.sample_rate = format["sample_per_sec"].value
|
||||
|
||||
self.compression = format["codec"].display
|
||||
if "nb_sample/nb_sample" in wav \
|
||||
and 0 < format["sample_per_sec"].value:
|
||||
self.duration = timedelta(seconds=float(wav["nb_sample/nb_sample"].value) / format["sample_per_sec"].value)
|
||||
if format["codec"].value in UNCOMPRESSED_AUDIO:
|
||||
# Codec with fixed bit rate
|
||||
self.bit_rate = format["nb_channel"].value * format["bit_per_sample"].value * format["sample_per_sec"].value
|
||||
if not self.has("duration") \
|
||||
and "audio_data/size" in wav \
|
||||
and self.has("bit_rate"):
|
||||
duration = float(wav["audio_data/size"].value)*8 / self.get('bit_rate')
|
||||
self.duration = timedelta(seconds=duration)
|
||||
|
||||
def extractInfo(self, fieldset):
|
||||
for field in fieldset:
|
||||
if not field.is_field_set:
|
||||
continue
|
||||
if "tag" in field:
|
||||
if field["tag"].value == "LIST":
|
||||
self.extractInfo(field)
|
||||
else:
|
||||
self.processChunk(field)
|
||||
|
||||
@fault_tolerant
|
||||
def extractAVIVideo(self, header, meta):
|
||||
meta.compression = "%s (fourcc:\"%s\")" \
|
||||
% (header["fourcc"].display, makeUnicode(header["fourcc"].value))
|
||||
if header["rate"].value and header["scale"].value:
|
||||
fps = float(header["rate"].value) / header["scale"].value
|
||||
meta.frame_rate = fps
|
||||
if 0 < fps:
|
||||
self.duration = meta.duration = timedelta(seconds=float(header["length"].value) / fps)
|
||||
|
||||
if "../stream_fmt/width" in header:
|
||||
format = header["../stream_fmt"]
|
||||
meta.width = format["width"].value
|
||||
meta.height = format["height"].value
|
||||
meta.bits_per_pixel = format["depth"].value
|
||||
else:
|
||||
meta.width = header["right"].value - header["left"].value
|
||||
meta.height = header["bottom"].value - header["top"].value
|
||||
|
||||
@fault_tolerant
|
||||
def extractAVIAudio(self, format, meta):
|
||||
meta.nb_channel = format["channel"].value
|
||||
meta.sample_rate = format["sample_rate"].value
|
||||
meta.bit_rate = format["bit_rate"].value * 8
|
||||
if format["bits_per_sample"].value:
|
||||
meta.bits_per_sample = format["bits_per_sample"].value
|
||||
if "../stream_hdr" in format:
|
||||
header = format["../stream_hdr"]
|
||||
if header["rate"].value and header["scale"].value:
|
||||
frame_rate = float(header["rate"].value) / header["scale"].value
|
||||
meta.duration = timedelta(seconds=float(header["length"].value) / frame_rate)
|
||||
if header["fourcc"].value != "":
|
||||
meta.compression = "%s (fourcc:\"%s\")" \
|
||||
% (format["codec"].display, header["fourcc"].value)
|
||||
if not meta.has("compression"):
|
||||
meta.compression = format["codec"].display
|
||||
|
||||
self.computeAudioComprRate(meta)
|
||||
|
||||
@fault_tolerant
|
||||
def computeAudioComprRate(self, meta):
|
||||
uncompr = meta.get('bit_rate', 0)
|
||||
if not uncompr:
|
||||
return
|
||||
compr = meta.get('nb_channel') * meta.get('sample_rate') * meta.get('bits_per_sample', default=16)
|
||||
if not compr:
|
||||
return
|
||||
meta.compr_rate = float(compr) / uncompr
|
||||
|
||||
@fault_tolerant
|
||||
def useAviHeader(self, header):
|
||||
microsec = header["microsec_per_frame"].value
|
||||
if microsec:
|
||||
self.frame_rate = 1000000.0 / microsec
|
||||
total_frame = getValue(header, "total_frame")
|
||||
if total_frame and not self.has("duration"):
|
||||
self.duration = timedelta(microseconds=total_frame * microsec)
|
||||
self.width = header["width"].value
|
||||
self.height = header["height"].value
|
||||
|
||||
def extractAVI(self, headers):
|
||||
audio_index = 1
|
||||
for stream in headers.array("stream"):
|
||||
if "stream_hdr/stream_type" not in stream:
|
||||
continue
|
||||
stream_type = stream["stream_hdr/stream_type"].value
|
||||
if stream_type == "vids":
|
||||
if "stream_hdr" in stream:
|
||||
meta = Metadata(self)
|
||||
self.extractAVIVideo(stream["stream_hdr"], meta)
|
||||
self.addGroup("video", meta, "Video stream")
|
||||
elif stream_type == "auds":
|
||||
if "stream_fmt" in stream:
|
||||
meta = Metadata(self)
|
||||
self.extractAVIAudio(stream["stream_fmt"], meta)
|
||||
self.addGroup("audio[%u]" % audio_index, meta, "Audio stream")
|
||||
audio_index += 1
|
||||
if "avi_hdr" in headers:
|
||||
self.useAviHeader(headers["avi_hdr"])
|
||||
|
||||
# Compute global bit rate
|
||||
if self.has("duration") and "/movie/size" in headers:
|
||||
self.bit_rate = float(headers["/movie/size"].value) * 8 / timedelta2seconds(self.get('duration'))
|
||||
|
||||
# Video has index?
|
||||
if "/index" in headers:
|
||||
self.comment = _("Has audio/video index (%s)") \
|
||||
% humanFilesize(headers["/index"].size/8)
|
||||
|
||||
@fault_tolerant
|
||||
def extractAnim(self, riff):
|
||||
if "anim_rate/rate[0]" in riff:
|
||||
count = 0
|
||||
total = 0
|
||||
for rate in riff.array("anim_rate/rate"):
|
||||
count += 1
|
||||
if 100 < count:
|
||||
break
|
||||
total += rate.value / 60.0
|
||||
if count and total:
|
||||
self.frame_rate = count / total
|
||||
if not self.has("frame_rate") and "anim_hdr/jiffie_rate" in riff:
|
||||
self.frame_rate = 60.0 / riff["anim_hdr/jiffie_rate"].value
|
||||
|
||||
registerExtractor(RiffFile, RiffMetadata)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from hachoir_core.error import HACHOIR_ERRORS, warning
|
||||
|
||||
def fault_tolerant(func, *args):
|
||||
def safe_func(*args, **kw):
|
||||
try:
|
||||
func(*args, **kw)
|
||||
except HACHOIR_ERRORS, err:
|
||||
warning("Error when calling function %s(): %s" % (
|
||||
func.__name__, err))
|
||||
return safe_func
|
||||
|
||||
def getFieldAttribute(fieldset, key, attrname):
|
||||
try:
|
||||
field = fieldset[key]
|
||||
if field.hasValue():
|
||||
return getattr(field, attrname)
|
||||
except HACHOIR_ERRORS, err:
|
||||
warning("Unable to get %s of field %s/%s: %s" % (
|
||||
attrname, fieldset.path, key, err))
|
||||
return None
|
||||
|
||||
def getValue(fieldset, key):
|
||||
return getFieldAttribute(fieldset, key, "value")
|
||||
|
||||
def getDisplay(fieldset, key):
|
||||
return getFieldAttribute(fieldset, key, "display")
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
from datetime import date, datetime
|
||||
import re
|
||||
from hachoir_core.language import Language
|
||||
from locale import setlocale, LC_ALL
|
||||
from time import strptime
|
||||
from hachoir_metadata.timezone import createTimezone
|
||||
from hachoir_metadata import config
|
||||
|
||||
NORMALIZE_REGEX = re.compile("[-/.: ]+")
|
||||
YEAR_REGEX1 = re.compile("^([0-9]{4})$")
|
||||
|
||||
# Date regex: YYYY-MM-DD (US format)
|
||||
DATE_REGEX1 = re.compile("^([0-9]{4})~([01][0-9])~([0-9]{2})$")
|
||||
|
||||
# Date regex: YYYY-MM-DD HH:MM:SS (US format)
|
||||
DATETIME_REGEX1 = re.compile("^([0-9]{4})~([01][0-9])~([0-9]{2})~([0-9]{1,2})~([0-9]{2})~([0-9]{2})$")
|
||||
|
||||
# Datetime regex: "MM-DD-YYYY HH:MM:SS" (FR format)
|
||||
DATETIME_REGEX2 = re.compile("^([01]?[0-9])~([0-9]{2})~([0-9]{4})~([0-9]{1,2})~([0-9]{2})~([0-9]{2})$")
|
||||
|
||||
# Timezone regex: "(...) +0200"
|
||||
TIMEZONE_REGEX = re.compile("^(.*)~([+-][0-9]{2})00$")
|
||||
|
||||
# Timestmap: 'February 2007'
|
||||
MONTH_YEAR = "%B~%Y"
|
||||
|
||||
# Timestmap: 'Sun Feb 24 15:51:09 2008'
|
||||
RIFF_TIMESTAMP = "%a~%b~%d~%H~%M~%S~%Y"
|
||||
|
||||
# Timestmap: 'Thu, 19 Jul 2007 09:03:57'
|
||||
ISO_TIMESTAMP = "%a,~%d~%b~%Y~%H~%M~%S"
|
||||
|
||||
def parseDatetime(value):
|
||||
"""
|
||||
Year and date:
|
||||
>>> parseDatetime("2000")
|
||||
(datetime.date(2000, 1, 1), u'2000')
|
||||
>>> parseDatetime("2004-01-02")
|
||||
datetime.date(2004, 1, 2)
|
||||
|
||||
Timestamp:
|
||||
>>> parseDatetime("2004-01-02 18:10:45")
|
||||
datetime.datetime(2004, 1, 2, 18, 10, 45)
|
||||
>>> parseDatetime("2004-01-02 18:10:45")
|
||||
datetime.datetime(2004, 1, 2, 18, 10, 45)
|
||||
|
||||
Timestamp with timezone:
|
||||
>>> parseDatetime(u'Thu, 19 Jul 2007 09:03:57 +0000')
|
||||
datetime.datetime(2007, 7, 19, 9, 3, 57, tzinfo=<TimezoneUTC delta=0, name=u'UTC'>)
|
||||
>>> parseDatetime(u'Thu, 19 Jul 2007 09:03:57 +0200')
|
||||
datetime.datetime(2007, 7, 19, 9, 3, 57, tzinfo=<Timezone delta=2:00:00, name='+0200'>)
|
||||
"""
|
||||
value = NORMALIZE_REGEX.sub("~", value.strip())
|
||||
regs = YEAR_REGEX1.match(value)
|
||||
if regs:
|
||||
try:
|
||||
year = int(regs.group(1))
|
||||
return (date(year, 1, 1), unicode(year))
|
||||
except ValueError:
|
||||
pass
|
||||
regs = DATE_REGEX1.match(value)
|
||||
if regs:
|
||||
try:
|
||||
year = int(regs.group(1))
|
||||
month = int(regs.group(2))
|
||||
day = int(regs.group(3))
|
||||
return date(year, month, day)
|
||||
except ValueError:
|
||||
pass
|
||||
regs = DATETIME_REGEX1.match(value)
|
||||
if regs:
|
||||
try:
|
||||
year = int(regs.group(1))
|
||||
month = int(regs.group(2))
|
||||
day = int(regs.group(3))
|
||||
hour = int(regs.group(4))
|
||||
min = int(regs.group(5))
|
||||
sec = int(regs.group(6))
|
||||
return datetime(year, month, day, hour, min, sec)
|
||||
except ValueError:
|
||||
pass
|
||||
regs = DATETIME_REGEX2.match(value)
|
||||
if regs:
|
||||
try:
|
||||
month = int(regs.group(1))
|
||||
day = int(regs.group(2))
|
||||
year = int(regs.group(3))
|
||||
hour = int(regs.group(4))
|
||||
min = int(regs.group(5))
|
||||
sec = int(regs.group(6))
|
||||
return datetime(year, month, day, hour, min, sec)
|
||||
except ValueError:
|
||||
pass
|
||||
current_locale = setlocale(LC_ALL, "C")
|
||||
try:
|
||||
match = TIMEZONE_REGEX.match(value)
|
||||
if match:
|
||||
without_timezone = match.group(1)
|
||||
delta = int(match.group(2))
|
||||
delta = createTimezone(delta)
|
||||
else:
|
||||
without_timezone = value
|
||||
delta = None
|
||||
try:
|
||||
timestamp = strptime(without_timezone, ISO_TIMESTAMP)
|
||||
arguments = list(timestamp[0:6]) + [0, delta]
|
||||
return datetime(*arguments)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
timestamp = strptime(without_timezone, RIFF_TIMESTAMP)
|
||||
arguments = list(timestamp[0:6]) + [0, delta]
|
||||
return datetime(*arguments)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
timestamp = strptime(value, MONTH_YEAR)
|
||||
arguments = list(timestamp[0:3])
|
||||
return date(*arguments)
|
||||
except ValueError:
|
||||
pass
|
||||
finally:
|
||||
setlocale(LC_ALL, current_locale)
|
||||
return None
|
||||
|
||||
def setDatetime(meta, key, value):
|
||||
if isinstance(value, (str, unicode)):
|
||||
return parseDatetime(value)
|
||||
elif isinstance(value, (date, datetime)):
|
||||
return value
|
||||
return None
|
||||
|
||||
def setLanguage(meta, key, value):
|
||||
"""
|
||||
>>> setLanguage(None, None, "fre")
|
||||
<Language 'French', code='fre'>
|
||||
>>> setLanguage(None, None, u"ger")
|
||||
<Language 'German', code='ger'>
|
||||
"""
|
||||
return Language(value)
|
||||
|
||||
def setTrackTotal(meta, key, total):
|
||||
"""
|
||||
>>> setTrackTotal(None, None, "10")
|
||||
10
|
||||
"""
|
||||
try:
|
||||
return int(total)
|
||||
except ValueError:
|
||||
meta.warning("Invalid track total: %r" % total)
|
||||
return None
|
||||
|
||||
def setTrackNumber(meta, key, number):
|
||||
if isinstance(number, (int, long)):
|
||||
return number
|
||||
if "/" in number:
|
||||
number, total = number.split("/", 1)
|
||||
meta.track_total = total
|
||||
try:
|
||||
return int(number)
|
||||
except ValueError:
|
||||
meta.warning("Invalid track number: %r" % number)
|
||||
return None
|
||||
|
||||
def normalizeString(text):
|
||||
if config.RAW_OUTPUT:
|
||||
return text
|
||||
return text.strip(" \t\v\n\r\0")
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
from datetime import tzinfo, timedelta
|
||||
|
||||
class TimezoneUTC(tzinfo):
|
||||
"""UTC timezone"""
|
||||
ZERO = timedelta(0)
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return TimezoneUTC.ZERO
|
||||
|
||||
def tzname(self, dt):
|
||||
return u"UTC"
|
||||
|
||||
def dst(self, dt):
|
||||
return TimezoneUTC.ZERO
|
||||
|
||||
def __repr__(self):
|
||||
return "<TimezoneUTC delta=0, name=u'UTC'>"
|
||||
|
||||
class Timezone(TimezoneUTC):
|
||||
"""Fixed offset in hour from UTC."""
|
||||
def __init__(self, offset):
|
||||
self._offset = timedelta(minutes=offset*60)
|
||||
self._name = u"%+03u00" % offset
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return self._offset
|
||||
|
||||
def tzname(self, dt):
|
||||
return self._name
|
||||
|
||||
def __repr__(self):
|
||||
return "<Timezone delta=%s, name='%s'>" % (
|
||||
self._offset, self._name)
|
||||
|
||||
UTC = TimezoneUTC()
|
||||
|
||||
def createTimezone(offset):
|
||||
if offset:
|
||||
return Timezone(offset)
|
||||
else:
|
||||
return UTC
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
PACKAGE = "hachoir-metadata"
|
||||
VERSION = "1.3.3"
|
||||
WEBSITE = "http://bitbucket.org/haypo/hachoir/wiki/hachoir-metadata"
|
||||
LICENSE = "GNU GPL v2"
|
||||
|
||||
@@ -0,0 +1,412 @@
|
||||
from hachoir_core.field import MissingField
|
||||
from hachoir_metadata.metadata import (registerExtractor,
|
||||
Metadata, RootMetadata, MultipleMetadata)
|
||||
from hachoir_metadata.metadata_item import QUALITY_GOOD
|
||||
from hachoir_metadata.safe import fault_tolerant
|
||||
from hachoir_parser.video import MovFile, AsfFile, FlvFile
|
||||
from hachoir_parser.video.asf import Descriptor as ASF_Descriptor
|
||||
from hachoir_parser.container import MkvFile
|
||||
from hachoir_parser.container.mkv import dateToDatetime
|
||||
from hachoir_core.i18n import _
|
||||
from hachoir_core.tools import makeUnicode, makePrintable, timedelta2seconds
|
||||
from datetime import timedelta
|
||||
|
||||
class MkvMetadata(MultipleMetadata):
|
||||
tag_key = {
|
||||
"TITLE": "title",
|
||||
"URL": "url",
|
||||
"COPYRIGHT": "copyright",
|
||||
|
||||
# TODO: use maybe another name?
|
||||
# Its value may be different than (...)/Info/DateUTC/date
|
||||
"DATE_RECORDED": "creation_date",
|
||||
|
||||
# TODO: Extract subtitle metadata
|
||||
"SUBTITLE": "subtitle_author",
|
||||
}
|
||||
|
||||
def extract(self, mkv):
|
||||
for segment in mkv.array("Segment"):
|
||||
self.processSegment(segment)
|
||||
|
||||
def processSegment(self, segment):
|
||||
for field in segment:
|
||||
if field.name.startswith("Info["):
|
||||
self.processInfo(field)
|
||||
elif field.name.startswith("Tags["):
|
||||
for tag in field.array("Tag"):
|
||||
self.processTag(tag)
|
||||
elif field.name.startswith("Tracks["):
|
||||
self.processTracks(field)
|
||||
elif field.name.startswith("Cluster["):
|
||||
if self.quality < QUALITY_GOOD:
|
||||
return
|
||||
|
||||
def processTracks(self, tracks):
|
||||
for entry in tracks.array("TrackEntry"):
|
||||
self.processTrack(entry)
|
||||
|
||||
def processTrack(self, track):
|
||||
if "TrackType/enum" not in track:
|
||||
return
|
||||
if track["TrackType/enum"].display == "video":
|
||||
self.processVideo(track)
|
||||
elif track["TrackType/enum"].display == "audio":
|
||||
self.processAudio(track)
|
||||
elif track["TrackType/enum"].display == "subtitle":
|
||||
self.processSubtitle(track)
|
||||
|
||||
def trackCommon(self, track, meta):
|
||||
if "Name/unicode" in track:
|
||||
meta.title = track["Name/unicode"].value
|
||||
if "Language/string" in track \
|
||||
and track["Language/string"].value not in ("mis", "und"):
|
||||
meta.language = track["Language/string"].value
|
||||
|
||||
def processVideo(self, track):
|
||||
video = Metadata(self)
|
||||
self.trackCommon(track, video)
|
||||
try:
|
||||
video.compression = track["CodecID/string"].value
|
||||
if "Video" in track:
|
||||
video.width = track["Video/PixelWidth/unsigned"].value
|
||||
video.height = track["Video/PixelHeight/unsigned"].value
|
||||
except MissingField:
|
||||
pass
|
||||
self.addGroup("video[]", video, "Video stream")
|
||||
|
||||
def getDouble(self, field, parent):
|
||||
float_key = '%s/float' % parent
|
||||
if float_key in field:
|
||||
return field[float_key].value
|
||||
double_key = '%s/double' % parent
|
||||
if double_key in field:
|
||||
return field[double_key].value
|
||||
return None
|
||||
|
||||
def processAudio(self, track):
|
||||
audio = Metadata(self)
|
||||
self.trackCommon(track, audio)
|
||||
if "Audio" in track:
|
||||
frequency = self.getDouble(track, "Audio/SamplingFrequency")
|
||||
if frequency is not None:
|
||||
audio.sample_rate = frequency
|
||||
if "Audio/Channels/unsigned" in track:
|
||||
audio.nb_channel = track["Audio/Channels/unsigned"].value
|
||||
if "Audio/BitDepth/unsigned" in track:
|
||||
audio.bits_per_sample = track["Audio/BitDepth/unsigned"].value
|
||||
if "CodecID/string" in track:
|
||||
audio.compression = track["CodecID/string"].value
|
||||
self.addGroup("audio[]", audio, "Audio stream")
|
||||
|
||||
def processSubtitle(self, track):
|
||||
sub = Metadata(self)
|
||||
self.trackCommon(track, sub)
|
||||
try:
|
||||
sub.compression = track["CodecID/string"].value
|
||||
except MissingField:
|
||||
pass
|
||||
self.addGroup("subtitle[]", sub, "Subtitle")
|
||||
|
||||
def processTag(self, tag):
|
||||
for field in tag.array("SimpleTag"):
|
||||
self.processSimpleTag(field)
|
||||
|
||||
def processSimpleTag(self, tag):
|
||||
if "TagName/unicode" not in tag \
|
||||
or "TagString/unicode" not in tag:
|
||||
return
|
||||
name = tag["TagName/unicode"].value
|
||||
if name not in self.tag_key:
|
||||
return
|
||||
key = self.tag_key[name]
|
||||
value = tag["TagString/unicode"].value
|
||||
setattr(self, key, value)
|
||||
|
||||
def processInfo(self, info):
|
||||
if "TimecodeScale/unsigned" in info:
|
||||
duration = self.getDouble(info, "Duration")
|
||||
if duration is not None:
|
||||
try:
|
||||
seconds = duration * info["TimecodeScale/unsigned"].value * 1e-9
|
||||
self.duration = timedelta(seconds=seconds)
|
||||
except OverflowError:
|
||||
# Catch OverflowError for timedelta (long int too large
|
||||
# to be converted to an int)
|
||||
pass
|
||||
if "DateUTC/date" in info:
|
||||
try:
|
||||
self.creation_date = dateToDatetime(info["DateUTC/date"].value)
|
||||
except OverflowError:
|
||||
pass
|
||||
if "WritingApp/unicode" in info:
|
||||
self.producer = info["WritingApp/unicode"].value
|
||||
if "MuxingApp/unicode" in info:
|
||||
self.producer = info["MuxingApp/unicode"].value
|
||||
if "Title/unicode" in info:
|
||||
self.title = info["Title/unicode"].value
|
||||
|
||||
class FlvMetadata(MultipleMetadata):
|
||||
def extract(self, flv):
|
||||
if "video[0]" in flv:
|
||||
meta = Metadata(self)
|
||||
self.extractVideo(flv["video[0]"], meta)
|
||||
self.addGroup("video", meta, "Video stream")
|
||||
if "audio[0]" in flv:
|
||||
meta = Metadata(self)
|
||||
self.extractAudio(flv["audio[0]"], meta)
|
||||
self.addGroup("audio", meta, "Audio stream")
|
||||
# TODO: Computer duration
|
||||
# One technic: use last video/audio chunk and use timestamp
|
||||
# But this is very slow
|
||||
self.format_version = flv.description
|
||||
|
||||
if "metadata/entry[1]" in flv:
|
||||
self.extractAMF(flv["metadata/entry[1]"])
|
||||
if self.has('duration'):
|
||||
self.bit_rate = flv.size / timedelta2seconds(self.get('duration'))
|
||||
|
||||
@fault_tolerant
|
||||
def extractAudio(self, audio, meta):
|
||||
if audio["codec"].display == "MP3" and "music_data" in audio:
|
||||
meta.compression = audio["music_data"].description
|
||||
else:
|
||||
meta.compression = audio["codec"].display
|
||||
meta.sample_rate = audio.getSampleRate()
|
||||
if audio["is_16bit"].value:
|
||||
meta.bits_per_sample = 16
|
||||
else:
|
||||
meta.bits_per_sample = 8
|
||||
if audio["is_stereo"].value:
|
||||
meta.nb_channel = 2
|
||||
else:
|
||||
meta.nb_channel = 1
|
||||
|
||||
@fault_tolerant
|
||||
def extractVideo(self, video, meta):
|
||||
meta.compression = video["codec"].display
|
||||
|
||||
def extractAMF(self, amf):
|
||||
for entry in amf.array("item"):
|
||||
self.useAmfEntry(entry)
|
||||
|
||||
@fault_tolerant
|
||||
def useAmfEntry(self, entry):
|
||||
key = entry["key"].value
|
||||
if key == "duration":
|
||||
self.duration = timedelta(seconds=entry["value"].value)
|
||||
elif key == "creator":
|
||||
self.producer = entry["value"].value
|
||||
elif key == "audiosamplerate":
|
||||
self.sample_rate = entry["value"].value
|
||||
elif key == "framerate":
|
||||
self.frame_rate = entry["value"].value
|
||||
elif key == "metadatacreator":
|
||||
self.producer = entry["value"].value
|
||||
elif key == "metadatadate":
|
||||
self.creation_date = entry.value
|
||||
elif key == "width":
|
||||
self.width = int(entry["value"].value)
|
||||
elif key == "height":
|
||||
self.height = int(entry["value"].value)
|
||||
|
||||
class MovMetadata(RootMetadata):
|
||||
def extract(self, mov):
|
||||
for atom in mov:
|
||||
if "movie" in atom:
|
||||
self.processMovie(atom["movie"])
|
||||
|
||||
@fault_tolerant
|
||||
def processMovieHeader(self, hdr):
|
||||
self.creation_date = hdr["creation_date"].value
|
||||
self.last_modification = hdr["lastmod_date"].value
|
||||
self.duration = timedelta(seconds=float(hdr["duration"].value) / hdr["time_scale"].value)
|
||||
self.comment = _("Play speed: %.1f%%") % (hdr["play_speed"].value*100)
|
||||
self.comment = _("User volume: %.1f%%") % (float(hdr["volume"].value)*100//255)
|
||||
|
||||
@fault_tolerant
|
||||
def processTrackHeader(self, hdr):
|
||||
width = int(hdr["frame_size_width"].value)
|
||||
height = int(hdr["frame_size_height"].value)
|
||||
if width and height:
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def processTrack(self, atom):
|
||||
for field in atom:
|
||||
if "track_hdr" in field:
|
||||
self.processTrackHeader(field["track_hdr"])
|
||||
|
||||
def processMovie(self, atom):
|
||||
for field in atom:
|
||||
if "track" in field:
|
||||
self.processTrack(field["track"])
|
||||
if "movie_hdr" in field:
|
||||
self.processMovieHeader(field["movie_hdr"])
|
||||
|
||||
|
||||
class AsfMetadata(MultipleMetadata):
|
||||
EXT_DESC_TO_ATTR = {
|
||||
"Encoder": "producer",
|
||||
"ToolName": "producer",
|
||||
"AlbumTitle": "album",
|
||||
"Track": "track_number",
|
||||
"TrackNumber": "track_total",
|
||||
"Year": "creation_date",
|
||||
"AlbumArtist": "author",
|
||||
}
|
||||
SKIP_EXT_DESC = set((
|
||||
# Useless informations
|
||||
"WMFSDKNeeded", "WMFSDKVersion",
|
||||
"Buffer Average", "VBR Peak", "EncodingTime",
|
||||
"MediaPrimaryClassID", "UniqueFileIdentifier",
|
||||
))
|
||||
|
||||
def extract(self, asf):
|
||||
if "header/content" in asf:
|
||||
self.processHeader(asf["header/content"])
|
||||
|
||||
def processHeader(self, header):
|
||||
compression = []
|
||||
is_vbr = None
|
||||
|
||||
if "ext_desc/content" in header:
|
||||
# Extract all data from ext_desc
|
||||
data = {}
|
||||
for desc in header.array("ext_desc/content/descriptor"):
|
||||
self.useExtDescItem(desc, data)
|
||||
|
||||
# Have ToolName and ToolVersion? If yes, group them to producer key
|
||||
if "ToolName" in data and "ToolVersion" in data:
|
||||
self.producer = "%s (version %s)" % (data["ToolName"], data["ToolVersion"])
|
||||
del data["ToolName"]
|
||||
del data["ToolVersion"]
|
||||
|
||||
# "IsVBR" key
|
||||
if "IsVBR" in data:
|
||||
is_vbr = (data["IsVBR"] == 1)
|
||||
del data["IsVBR"]
|
||||
|
||||
# Store data
|
||||
for key, value in data.iteritems():
|
||||
if key in self.EXT_DESC_TO_ATTR:
|
||||
key = self.EXT_DESC_TO_ATTR[key]
|
||||
else:
|
||||
if isinstance(key, str):
|
||||
key = makePrintable(key, "ISO-8859-1", to_unicode=True)
|
||||
value = "%s=%s" % (key, value)
|
||||
key = "comment"
|
||||
setattr(self, key, value)
|
||||
|
||||
if "file_prop/content" in header:
|
||||
self.useFileProp(header["file_prop/content"], is_vbr)
|
||||
|
||||
if "codec_list/content" in header:
|
||||
for codec in header.array("codec_list/content/codec"):
|
||||
if "name" in codec:
|
||||
text = codec["name"].value
|
||||
if "desc" in codec and codec["desc"].value:
|
||||
text = "%s (%s)" % (text, codec["desc"].value)
|
||||
compression.append(text)
|
||||
|
||||
audio_index = 1
|
||||
video_index = 1
|
||||
for index, stream_prop in enumerate(header.array("stream_prop")):
|
||||
if "content/audio_header" in stream_prop:
|
||||
meta = Metadata(self)
|
||||
self.streamProperty(header, index, meta)
|
||||
self.streamAudioHeader(stream_prop["content/audio_header"], meta)
|
||||
if self.addGroup("audio[%u]" % audio_index, meta, "Audio stream #%u" % audio_index):
|
||||
audio_index += 1
|
||||
elif "content/video_header" in stream_prop:
|
||||
meta = Metadata(self)
|
||||
self.streamProperty(header, index, meta)
|
||||
self.streamVideoHeader(stream_prop["content/video_header"], meta)
|
||||
if self.addGroup("video[%u]" % video_index, meta, "Video stream #%u" % video_index):
|
||||
video_index += 1
|
||||
|
||||
if "metadata/content" in header:
|
||||
info = header["metadata/content"]
|
||||
try:
|
||||
self.title = info["title"].value
|
||||
self.author = info["author"].value
|
||||
self.copyright = info["copyright"].value
|
||||
except MissingField:
|
||||
pass
|
||||
|
||||
@fault_tolerant
|
||||
def streamAudioHeader(self, audio, meta):
|
||||
if not meta.has("compression"):
|
||||
meta.compression = audio["twocc"].display
|
||||
meta.nb_channel = audio["channels"].value
|
||||
meta.sample_rate = audio["sample_rate"].value
|
||||
meta.bits_per_sample = audio["bits_per_sample"].value
|
||||
|
||||
@fault_tolerant
|
||||
def streamVideoHeader(self, video, meta):
|
||||
meta.width = video["width"].value
|
||||
meta.height = video["height"].value
|
||||
if "bmp_info" in video:
|
||||
bmp_info = video["bmp_info"]
|
||||
if not meta.has("compression"):
|
||||
meta.compression = bmp_info["codec"].display
|
||||
meta.bits_per_pixel = bmp_info["bpp"].value
|
||||
|
||||
@fault_tolerant
|
||||
def useExtDescItem(self, desc, data):
|
||||
if desc["type"].value == ASF_Descriptor.TYPE_BYTE_ARRAY:
|
||||
# Skip binary data
|
||||
return
|
||||
key = desc["name"].value
|
||||
if "/" in key:
|
||||
# Replace "WM/ToolName" with "ToolName"
|
||||
key = key.split("/", 1)[1]
|
||||
if key in self.SKIP_EXT_DESC:
|
||||
# Skip some keys
|
||||
return
|
||||
value = desc["value"].value
|
||||
if not value:
|
||||
return
|
||||
value = makeUnicode(value)
|
||||
data[key] = value
|
||||
|
||||
@fault_tolerant
|
||||
def useFileProp(self, prop, is_vbr):
|
||||
self.creation_date = prop["creation_date"].value
|
||||
self.duration = prop["play_duration"].value
|
||||
if prop["seekable"].value:
|
||||
self.comment = u"Is seekable"
|
||||
value = prop["max_bitrate"].value
|
||||
text = prop["max_bitrate"].display
|
||||
if is_vbr is True:
|
||||
text = "VBR (%s max)" % text
|
||||
elif is_vbr is False:
|
||||
text = "%s (CBR)" % text
|
||||
else:
|
||||
text = "%s (max)" % text
|
||||
self.bit_rate = (value, text)
|
||||
|
||||
def streamProperty(self, header, index, meta):
|
||||
key = "bit_rates/content/bit_rate[%u]/avg_bitrate" % index
|
||||
if key in header:
|
||||
meta.bit_rate = header[key].value
|
||||
|
||||
# TODO: Use codec list
|
||||
# It doesn't work when the video uses /header/content/bitrate_mutex
|
||||
# since the codec list are shared between streams but... how is it
|
||||
# shared?
|
||||
# key = "codec_list/content/codec[%u]" % index
|
||||
# if key in header:
|
||||
# codec = header[key]
|
||||
# if "name" in codec:
|
||||
# text = codec["name"].value
|
||||
# if "desc" in codec and codec["desc"].value:
|
||||
# meta.compression = "%s (%s)" % (text, codec["desc"].value)
|
||||
# else:
|
||||
# meta.compression = text
|
||||
|
||||
registerExtractor(MovFile, MovMetadata)
|
||||
registerExtractor(AsfFile, AsfMetadata)
|
||||
registerExtractor(FlvFile, FlvMetadata)
|
||||
registerExtractor(MkvFile, MkvMetadata)
|
||||
|
||||
Reference in new issue
Block a user