mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-09 11:14:08 +08:00
Merge remote-tracking branch 'origin/master' into calibre_cover_generate
This commit is contained in:
@@ -1471,6 +1471,8 @@ extra_titlepage_entries:readings,awards
|
||||
awards_label:Awards
|
||||
readings_label:Readings
|
||||
|
||||
## some sites include images that we don't ever want becoming the
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:art/.*Awards.jpg
|
||||
|
||||
[voracity2.e-fic.com]
|
||||
@@ -1667,6 +1669,10 @@ dateUpdated_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
## to on, but can be switched off if it is found to cause problems.
|
||||
fix_fimf_blockquotes:true
|
||||
|
||||
## some sites include images that we don't ever want becoming the
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/images/emoticons/
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:My Little Pony: Friendship is Magic
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
# Adolfo Jayme Barrientos, 2014
|
||||
# dario hereñu <magallania@gmail.com>, 2015
|
||||
# Jellby <jellby@yahoo.com>, 2014-2015
|
||||
# JimmXinu <retiefjimm@gmail.com>, 2015
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: calibre-plugins\n"
|
||||
"POT-Creation-Date: 2015-04-11 11:17+Central Daylight Time\n"
|
||||
"PO-Revision-Date: 2015-04-17 13:02+0000\n"
|
||||
"Last-Translator: Jellby <jellby@yahoo.com>\n"
|
||||
"PO-Revision-Date: 2015-04-22 22:08+0000\n"
|
||||
"Last-Translator: JimmXinu <retiefjimm@gmail.com>\n"
|
||||
"Language-Team: Spanish (http://www.transifex.com/projects/p/calibre-plugins/language/es/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -558,7 +559,7 @@ msgid ""
|
||||
"The %(pini)s parameter %(gcset)s allows you to choose a GC setting based on "
|
||||
"metadata rather than site, but it's much more complex.<br \\>%(gcset)s is "
|
||||
"ignored when this is off."
|
||||
msgstr "El parámetro %(gcset)s de %(pini) le permite elegir una configuración de GC según los metadatos en vez del sitio, pero es mucho más complejo.<br>%(gcset)s no se tiene en cuenta si esta opción está desactivada."
|
||||
msgstr "El parámetro %(gcset)s de %(pini)s le permite elegir una configuración de GC según los metadatos en vez del sitio, pero es mucho más complejo.<br>%(gcset)s no se tiene en cuenta si esta opción está desactivada."
|
||||
|
||||
#: config.py:811
|
||||
msgid "Use calibre's Polish feature to inject/update the cover"
|
||||
@@ -794,7 +795,7 @@ msgid ""
|
||||
"The %(pini)s parameter %(ccset)s allows you to set custom columns to site "
|
||||
"specific values that aren't common to all sites.<br />%(ccset)s is ignored "
|
||||
"when this is off."
|
||||
msgstr "El parámetro %(ccset)s de %(pini) le permite asignar a las columnas personalizadas valores específicos para cada sitio.<br>%(ccset)s no se tiene en cuenta si esta opción está desactivada."
|
||||
msgstr "El parámetro %(ccset)s de %(pini)s le permite asignar a las columnas personalizadas valores específicos para cada sitio.<br>%(ccset)s no se tiene en cuenta si esta opción está desactivada."
|
||||
|
||||
#: config.py:1065
|
||||
msgid "Special column:"
|
||||
|
||||
@@ -176,7 +176,10 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
if coverurl.startswith('//'): # fix for img urls missing 'http:'
|
||||
coverurl = "http:"+coverurl
|
||||
if get_cover:
|
||||
self.setCoverImage(self.url,coverurl)
|
||||
# try setting from href, if fails, try using the img src
|
||||
if self.setCoverImage(self.url,coverurl)[0] == "failedtoload":
|
||||
coverurl = storyImage.find('img')['src']
|
||||
self.setCoverImage(self.url,coverurl)
|
||||
|
||||
coverSource = storyImage.find('a', {'class':'source'})
|
||||
if coverSource:
|
||||
|
||||
@@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
#from .. import BeautifulSoup as bs
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
@@ -144,7 +144,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Error! The story you're trying to access is being filtered by your choice of contents filtering.")
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = bs.BeautifulSoup(data)
|
||||
soup = self.make_soup(data)
|
||||
#print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
@@ -178,7 +178,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
page=0
|
||||
i=0
|
||||
while i == 0:
|
||||
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getList('authorUrl')[0]+"/"+str(page)))
|
||||
asoup = self.make_soup(self._fetchUrl(self.story.getList('authorUrl')[0]+"/"+str(page)))
|
||||
|
||||
a = asoup.findAll('td', {'class' : 'lc2'})
|
||||
for lc2 in a:
|
||||
@@ -212,7 +212,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('seriesUrl',seriesUrl)
|
||||
series_name = stripHTML(a)
|
||||
logger.debug("Series name= %s" % series_name)
|
||||
series_soup = bs.BeautifulSoup(self._fetchUrl(seriesUrl))
|
||||
series_soup = self.make_soup(self._fetchUrl(seriesUrl))
|
||||
if series_soup:
|
||||
logger.debug("Retrieving Series - looking for name")
|
||||
series_name = series_soup.find('span', {'id' : 'ptitle'}).text.partition(' — ')[0]
|
||||
@@ -221,7 +221,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
desc = lc4.contents[2]
|
||||
# Check if series is in a universe
|
||||
universe_url = self.story.getList('authorUrl')[0] + "&type=uni"
|
||||
universes_soup = bs.BeautifulSoup(self._fetchUrl(universe_url) )
|
||||
universes_soup = self.make_soup(self._fetchUrl(universe_url) )
|
||||
logger.debug("Universe url='{0}'".format(universe_url))
|
||||
if universes_soup:
|
||||
universes = universes_soup.findAll('div', {'class' : 'ser-box'})
|
||||
@@ -255,7 +255,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
universe_name = stripHTML(a)
|
||||
universeUrl = 'http://'+self.host+a['href']
|
||||
logger.debug("Retrieving Universe - about to get page - universeUrl='{0}".format(universeUrl))
|
||||
universe_soup = bs.BeautifulSoup(self._fetchUrl(universeUrl))
|
||||
universe_soup = self.make_soup(self._fetchUrl(universeUrl))
|
||||
logger.debug("Retrieving Universe - have page")
|
||||
if universe_soup:
|
||||
logger.debug("Retrieving Universe - looking for name")
|
||||
@@ -318,10 +318,12 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url),
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
soup = self.make_soup(self._fetchUrl(url))
|
||||
|
||||
div = soup.find('article', {'id' : 'story'})
|
||||
div = soup.find('div', {'id' : 'story'})
|
||||
if not div:
|
||||
logger.debug("div id=story not found, try article")
|
||||
div = soup.find('article', {'id' : 'story'})
|
||||
|
||||
# some big chapters are split over several pages
|
||||
pager = div.find('span', {'class' : 'pager'})
|
||||
@@ -340,10 +342,12 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
# logger.debug(div)
|
||||
|
||||
for ur in urls:
|
||||
soup = bs.BeautifulSoup(self._fetchUrl("http://"+self.getSiteDomain()+ur['href']),
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
soup = self.make_soup(self._fetchUrl("http://"+self.getSiteDomain()+ur['href']))
|
||||
|
||||
div1 = soup.find('article', {'id' : 'story'})
|
||||
div1 = soup.find('div', {'id' : 'story'})
|
||||
if not div1:
|
||||
logger.debug("div id=story not found, try article")
|
||||
div1 = soup.find('article', {'id' : 'story'})
|
||||
|
||||
# Find the "Continues" marker on the current page and remove everything after that.
|
||||
continues = div.find('span', {'class' : 'conTag'})
|
||||
@@ -368,7 +372,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
if endPager != None:
|
||||
b = endPager.nextSibling
|
||||
while endPager != None:
|
||||
logger.debug("removing end: {0}".format(endPager))
|
||||
# logger.debug("removing end: {0}".format(endPager))
|
||||
b = endPager.nextSibling
|
||||
endPager.extract()
|
||||
endPager = b
|
||||
@@ -401,21 +405,35 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
|
||||
# For a story or the last chapter, remove voting form and the in library box
|
||||
a = div.find('div', {'id' : 'vote-form'})
|
||||
if a != None:
|
||||
a.extract()
|
||||
a = div.find('div', {'id' : 'top-header'})
|
||||
if a != None:
|
||||
a.extract()
|
||||
a = div.find('div', {'id' : 'b-man-div'})
|
||||
if a != None:
|
||||
a.extract()
|
||||
|
||||
# Kill the "The End" header and everything after it.
|
||||
a = div.find(['h2', 'h3'], {'class' : 'end'})
|
||||
logger.debug("Chapter end= '{0}'".format(a))
|
||||
# Kill the vote form and everything after it.
|
||||
a = div.find('div', {'class' : 'vform'})
|
||||
# logger.debug("Chapter end= '{0}'".format(a))
|
||||
while a != None:
|
||||
b = a.nextSibling
|
||||
a.extract()
|
||||
a=b
|
||||
|
||||
# Kill the vote form and everything after it.
|
||||
a = div.find('h3', {'class' : 'end'})
|
||||
# logger.debug("Chapter end= '{0}'".format(a))
|
||||
while a != None:
|
||||
b = a.nextSibling
|
||||
a.extract()
|
||||
a=b
|
||||
|
||||
foot = div.find('footer')
|
||||
if foot != None:
|
||||
foot.extract()
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
|
||||
@@ -481,8 +481,10 @@ class BaseSiteAdapter(Configurable):
|
||||
|
||||
def setCoverImage(self,storyurl,imgurl):
|
||||
if self.getConfig('include_images'):
|
||||
self.story.addImgUrl(storyurl,imgurl,self._fetchUrlRaw,cover=True,
|
||||
coverexclusion=self.getConfig('cover_exclusion_regexp'))
|
||||
return self.story.addImgUrl(storyurl,imgurl,self._fetchUrlRaw,cover=True,
|
||||
coverexclusion=self.getConfig('cover_exclusion_regexp'))
|
||||
else:
|
||||
return (None,None)
|
||||
|
||||
# bs3 & bs4 are different here.
|
||||
# will move to a bs3 vs bs4 block if there's lots of changes.
|
||||
|
||||
+14
-8
@@ -144,14 +144,20 @@ def main(argv=None, parser=None, passed_defaultsini=None, passed_personalini=Non
|
||||
else:
|
||||
urls = args
|
||||
|
||||
for url in urls:
|
||||
try:
|
||||
do_download(url,
|
||||
options,
|
||||
passed_defaultsini,
|
||||
passed_personalini)
|
||||
except Exception, e:
|
||||
print "URL(%s) Failed: Exception (%s). Run URL individually for more detail."%(url,e)
|
||||
if len(urls) > 1:
|
||||
for url in urls:
|
||||
try:
|
||||
do_download(url,
|
||||
options,
|
||||
passed_defaultsini,
|
||||
passed_personalini)
|
||||
except Exception, e:
|
||||
print "URL(%s) Failed: Exception (%s). Run URL individually for more detail."%(url,e)
|
||||
else:
|
||||
do_download(urls[0],
|
||||
options,
|
||||
passed_defaultsini,
|
||||
passed_personalini)
|
||||
|
||||
# make rest a function and loop on it.
|
||||
def do_download(arg,
|
||||
|
||||
@@ -1656,6 +1656,10 @@ dateUpdated_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
## to on, but can be switched off if it is found to cause problems.
|
||||
fix_fimf_blockquotes:true
|
||||
|
||||
## some sites include images that we don't ever want becoming the
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/images/emoticons/
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:My Little Pony: Friendship is Magic
|
||||
|
||||
|
||||
@@ -24,6 +24,13 @@ class FailedToDownload(Exception):
|
||||
def __str__(self):
|
||||
return self.error
|
||||
|
||||
class RejectImage(Exception):
|
||||
def __init__(self,error):
|
||||
self.error=error
|
||||
|
||||
def __str__(self):
|
||||
return self.error
|
||||
|
||||
class InvalidStoryURL(Exception):
|
||||
def __init__(self,url,domain,example):
|
||||
self.url=url
|
||||
|
||||
+19
-13
@@ -44,6 +44,8 @@ imagetypes = {
|
||||
|
||||
try:
|
||||
from calibre.utils.magick import Image
|
||||
from StringIO import StringIO
|
||||
from gif import GifInfo, CHECK_IS_ANIMATED
|
||||
convtype = {'jpg':'JPG', 'png':'PNG'}
|
||||
|
||||
def convert_image(url,data,sizes,grayscale,
|
||||
@@ -55,6 +57,10 @@ try:
|
||||
owidth, oheight = img.size
|
||||
nwidth, nheight = sizes
|
||||
scaled, nwidth, nheight = fit_image(owidth, oheight, nwidth, nheight)
|
||||
|
||||
if normalize_format_name(img.format)=="gif" and GifInfo(StringIO(data),CHECK_IS_ANIMATED).frameCount > 1:
|
||||
raise exceptions.RejectImage("Animated gifs come out purely--not going to use it.")
|
||||
|
||||
if scaled:
|
||||
img.size = (nwidth, nheight)
|
||||
export = True
|
||||
@@ -224,7 +230,7 @@ langs = {
|
||||
"Devanagari":"hi",
|
||||
|
||||
## These are from/for AO3:
|
||||
|
||||
|
||||
u'العربية':'ar',
|
||||
u'беларуская':'be',
|
||||
u'Български език':'bg',
|
||||
@@ -325,7 +331,7 @@ class InExMatch:
|
||||
else:
|
||||
retval = self.match == value
|
||||
#print(">>>>>>>>>>>>>%s==%s r: %s,%s=%s"%(self.match,value,self.negate,retval, self.negate != retval))
|
||||
|
||||
|
||||
return self.negate != retval
|
||||
|
||||
def __str__(self):
|
||||
@@ -338,7 +344,7 @@ class InExMatch:
|
||||
else:
|
||||
s='='
|
||||
return u'InExMatch(%s %s%s %s)'%(self.keys,f,s,self.match)
|
||||
|
||||
|
||||
## metakey[,metakey]=~pattern
|
||||
## metakey[,metakey]==string
|
||||
## *for* part lines. Effect only when trailing conditional key=~regexp matches
|
||||
@@ -358,7 +364,7 @@ def set_in_ex_clude(setting):
|
||||
match = InExMatch(line)
|
||||
dest.append([match,condmatch])
|
||||
return dest
|
||||
|
||||
|
||||
## Two or three part lines. Two part effect everything.
|
||||
## Three part effect only those key(s) lists.
|
||||
## pattern=>replacement
|
||||
@@ -433,7 +439,7 @@ class Story(Configurable):
|
||||
|
||||
def join_list(self, key, vallist):
|
||||
return self.getConfig("join_string_"+key,u", ").replace(SPACE_REPLACE,' ').join(map(unicode, vallist))
|
||||
|
||||
|
||||
def setMetadata(self, key, value, condremoveentities=True):
|
||||
|
||||
# keep as list type, but set as only value.
|
||||
@@ -445,20 +451,20 @@ class Story(Configurable):
|
||||
self.metadata[key]=conditionalRemoveEntities(value)
|
||||
else:
|
||||
self.metadata[key]=value
|
||||
|
||||
|
||||
if key == "language":
|
||||
try:
|
||||
# getMetadata not just self.metadata[] to do replace_metadata.
|
||||
self.setMetadata('langcode',langs[self.getMetadata(key)])
|
||||
except:
|
||||
self.setMetadata('langcode','en')
|
||||
|
||||
|
||||
if key == 'dateUpdated' and value:
|
||||
# Last Update tags for Bill.
|
||||
self.addToList('lastupdate',value.strftime("Last Update Year/Month: %Y/%m"))
|
||||
self.addToList('lastupdate',value.strftime("Last Update: %Y/%m/%d"))
|
||||
|
||||
|
||||
|
||||
def do_in_ex_clude(self,which,value,key):
|
||||
if value and which in self.in_ex_cludes:
|
||||
include = 'include' in which
|
||||
@@ -487,7 +493,7 @@ class Story(Configurable):
|
||||
if include and keyfound and not found:
|
||||
value = None
|
||||
return value
|
||||
|
||||
|
||||
|
||||
def doReplacements(self,value,key,return_list=False,seen_list=[]):
|
||||
value = self.do_in_ex_clude('include_metadata_pre',value,key)
|
||||
@@ -526,7 +532,7 @@ class Story(Configurable):
|
||||
# print("replacement,value:%s,%s->%s"%(replacement,value,regexp.sub(replacement,value)))
|
||||
value = regexp.sub(replacement,value)
|
||||
retlist = [value]
|
||||
|
||||
|
||||
for val in retlist:
|
||||
retlist = map(partial(self.do_in_ex_clude,'include_metadata_post',key=key),retlist)
|
||||
retlist = map(partial(self.do_in_ex_clude,'exclude_metadata_post',key=key),retlist)
|
||||
@@ -610,7 +616,7 @@ class Story(Configurable):
|
||||
self.getMetadata('author', removeallentities, doreplacements)))
|
||||
|
||||
self.extendList("extratags",self.getConfigList("extratags"))
|
||||
|
||||
|
||||
if self.getMetadataRaw('seriesUrl'):
|
||||
self.setMetadata('seriesHTML',linkhtml%('series',self.getMetadata('seriesUrl', removeallentities, doreplacements),
|
||||
self.getMetadata('series', removeallentities, doreplacements)))
|
||||
@@ -703,10 +709,10 @@ class Story(Configurable):
|
||||
for val in retlist:
|
||||
newretlist.extend(self.doReplacements(val,listname,return_list=True))
|
||||
retlist = newretlist
|
||||
|
||||
|
||||
if removeallentities:
|
||||
retlist = map(removeAllEntities,retlist)
|
||||
|
||||
|
||||
retlist = filter( lambda x : x!=None and x!='' ,retlist)
|
||||
|
||||
# reorder ships so b/a and c/b/a become a/b and a/b/c. Only on '/',
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
A pure Python GIF metadata extractor.
|
||||
Supports adjustable detail to fine-tune performance.
|
||||
|
||||
Example code and full epydoc docstrings included.
|
||||
|
||||
Uses:
|
||||
- Identifying whether a GIF is static or animated.
|
||||
- Extracting the dimensions, pixel aspect ratio, number of frames, loop count,
|
||||
global palette or palette size, and background color.
|
||||
- Extracting comments and other plaintext.
|
||||
- Testing for various structural errors.
|
||||
|
||||
TODO:
|
||||
- Provide basic support for XMP Metadata extraction
|
||||
- http://en.wikipedia.org/wiki/Extensible_Metadata_Platform#Location_in_file_types
|
||||
- http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
||||
- Generate test GIF with http://code.google.com/p/python-xmp-toolkit/
|
||||
|
||||
Changelog:
|
||||
- 0.2.2: Audited the code and made some corrections.
|
||||
- 0.2.1: 40% speed improvement (went from 15 to 9 seconds for 1000 images)
|
||||
- 0.2.0: Feature-complete
|
||||
- 0.1.0: Initial release
|
||||
"""
|
||||
|
||||
__appname__ = "gif.py"
|
||||
__author__ = "Stephan Sokolow (deitarion/SSokolow)"
|
||||
__version__ = "0.2.2"
|
||||
__license__ = "PSF License 2.4 or higher (The Python License)"
|
||||
|
||||
#{ Check Types (enum, numerical ordering is significant)
|
||||
CHECK_IS_GIF_FILE = 0 #: Just check for a valid GIF header.
|
||||
CHECK_IS_ANIMATED = 1 #: Check whether the file has more than one frame.
|
||||
CHECK_COUNT_FRAMES = 2 #: Count the number of frames in the file.
|
||||
CHECK_PARSE_PALETTE = 3 #: Parse the palette and resolve the background color.
|
||||
CHECK_READ_COMMENTS = 4 #: Load comments (can sometimes be large) into the L{GifInfo} object.
|
||||
CHECK_READ_ALL_TEXT = 5 #: Also load the contents of Plain Text extension blocks.
|
||||
|
||||
CHECK_ALL = CHECK_READ_ALL_TEXT #: alias to allow for future modifications
|
||||
|
||||
#{ Warning Codes (bitfield)
|
||||
WARN_NONE = 0 #: No warnings
|
||||
WARN_BAD_IMG = 1 #: Corruption (of the [sub]block size field(s)) or truncation detected in an image block
|
||||
WARN_BAD_EXT = 2 #: Corruption (of the [sub]block size field(s)) or truncation detected in an extension block
|
||||
WARN_BAD_SIZE = 4 #: An image block specifies dimensions exceeding the global canvas size
|
||||
WARN_BAD_BGCOLOR = 8 #: The background color index specified is greater than the palette size
|
||||
WARN_EOF = 16 #: File is missing it's trailer. (Corrupt elsewhere, truncated, or breaking spec by using EOF as the terminator.)
|
||||
WARN_TRUNC = 32 #: File is definitively either truncated or corrupt. (An EOF was encountered part-way through a structure.)
|
||||
WARN_LOOP_POS = 64 #: Netscape Application Extension block (animation-control) was present but not first in the file.
|
||||
#}
|
||||
|
||||
import struct
|
||||
|
||||
#{ Structures used by GifInfo
|
||||
gifHeaderStruct = struct.Struct('<xxxxxxHHBBB') #: File header
|
||||
gifImageStruct = struct.Struct('<HHHHB') #: Image block header
|
||||
gifExtenStruct = struct.Struct('<BB') #: Top-level extension block header
|
||||
gifNetscapeStruct = struct.Struct('<BBH') #: NETSCAPE Loop-control sub-block
|
||||
gifColorTripleStruct = struct.Struct('<BBB') #: RGB palette element
|
||||
gifPlaintextStruct = struct.Struct('<BHHHHBBBB') #: Plain Text block header
|
||||
#}
|
||||
|
||||
class BadHeaderException(Exception):
|
||||
"""Raised when no valid GIF header is found"""
|
||||
|
||||
class GifInfo(object):
|
||||
"""A class for loading and storing metadata from GIF files.
|
||||
|
||||
Accepts paths and file-like objects.
|
||||
|
||||
When using L{CHECK_ALL}, this can also be used to walk past a valid
|
||||
GIF file in an un-delimited byte stream in order to identify the point at
|
||||
which the following file starts. (It doesn't C{fh.seek(0)} or C{fh.close()} after
|
||||
use)
|
||||
"""
|
||||
warnFlags = WARN_NONE #: A bit field of C{WARN_*} flags set by L{__init__}
|
||||
checkLevel = CHECK_ALL #: Default C{CHECK_*} level used by L{__init__}
|
||||
|
||||
#{ Pre-defined "unset" values for GIF Metadata
|
||||
path = None #: The path to the file, if one was passed to L{__init__}
|
||||
version = None #: C{87a} or C{89a}
|
||||
width = None
|
||||
height = None
|
||||
loopCount = None
|
||||
pixelAspect = None
|
||||
paletteSize = None #: Always calculated if a global palette is present
|
||||
palette = None #: The palette as a list of integer RGB tuples. Requires L{CHECK_PARSE_PALETTE}.
|
||||
bgColor = None #: Global background color as an RGB tuple. Requires L{CHECK_PARSE_PALETTE}.
|
||||
comments = None #: Text in Comment (0xFE) extension blocks as a list of strings. Requires L{CHECK_READ_COMMENTS}
|
||||
otherText = None #: Text in "Plain Text" (0x01) extension blocks as a list of strings. Requires L{CHECK_READ_ALL_TEXT}
|
||||
frameCount = 0
|
||||
#}
|
||||
|
||||
def __init__(self, fh, checkLevel=checkLevel):
|
||||
"""
|
||||
@param fh: A path or file-like object for a GIF file.
|
||||
@param checkLevel: A C{CHECK_*} constant.
|
||||
|
||||
@raises BadHeaderException: The given file lacks a valid GIF header.
|
||||
@raises IOError: The underlying C{open()} system call failed.
|
||||
"""
|
||||
self.checkLevel = checkLevel
|
||||
if isinstance(fh, basestring):
|
||||
self.path = fh
|
||||
fh = open(fh, 'rb')
|
||||
|
||||
header = fh.read(gifHeaderStruct.size)
|
||||
if len(header) < gifHeaderStruct.size:
|
||||
raise BadHeaderException("File is too small to be a GIF")
|
||||
|
||||
self.version = header[3:6]
|
||||
if header[0:3] != 'GIF' or self.version not in ['87a', '89a']:
|
||||
raise BadHeaderException("File does not have a recognizable GIF header")
|
||||
elif self.checkLevel <= CHECK_IS_GIF_FILE:
|
||||
return
|
||||
|
||||
self.width, self.height, GCTF_Byte, bgColor, self.pixelAspect = gifHeaderStruct.unpack(header)
|
||||
|
||||
if self.pixelAspect:
|
||||
self.pixelAspect = (self.pixelAspect + 15) / 64.0
|
||||
|
||||
rawPalette = self._getPalette(fh, GCTF_Byte)
|
||||
self.paletteSize = int(len(rawPalette) / 3)
|
||||
if self.checkLevel >= CHECK_PARSE_PALETTE:
|
||||
self.palette = []
|
||||
for pos in range(0, self.paletteSize):
|
||||
self.palette.append(gifColorTripleStruct.unpack_from(rawPalette, pos * 3))
|
||||
|
||||
if self.paletteSize and bgColor > self.paletteSize:
|
||||
self.warnFlags = self.warnFlags | WARN_BAD_BGCOLOR
|
||||
elif self.palette:
|
||||
self.bgColor = self.palette[bgColor]
|
||||
|
||||
# Iterate blocks
|
||||
self.firstBlock = True
|
||||
blocktype = self._read(fh, 1)
|
||||
while not blocktype == chr(0x3B) and not self.warnFlags & WARN_EOF:
|
||||
self._blockHandlers.get(blocktype, lambda x, y:'')(self, fh)
|
||||
if self.checkLevel <= CHECK_IS_ANIMATED and self.frameCount > 1:
|
||||
return
|
||||
|
||||
self.firstBlock = False
|
||||
blocktype = self._read(fh, 1)
|
||||
|
||||
del self.firstBlock
|
||||
|
||||
def _handleImageBlock(self, fh):
|
||||
""""""
|
||||
self.frameCount += 1
|
||||
try:
|
||||
x, y, w, h, LCTF_Byte = gifImageStruct.unpack(self._read(fh, gifImageStruct.size))
|
||||
except:
|
||||
self.warnFlags = self.warnFlags | WARN_EOF | WARN_TRUNC
|
||||
return
|
||||
|
||||
if x + w > self.width or y + h > self.height:
|
||||
self.warnFlags = self.warnFlags | WARN_BAD_SIZE
|
||||
|
||||
self._getPalette(fh, LCTF_Byte) # Skip the local color table if present
|
||||
fh.read(1) # Skip the LZW minimum code size.
|
||||
|
||||
# Skip content and test for the block terminator
|
||||
if not self._skipSubBlocks(fh): # For example, if it's a zero-length string like EOF would return.
|
||||
self.warnFlags = self.warnFlags | WARN_BAD_IMG
|
||||
|
||||
def _handleGenericExtensionBlock(self, fh):
|
||||
"""@todo: Rewrite this so extension block types have method handlers."""
|
||||
try:
|
||||
extType, blkSize = gifExtenStruct.unpack(self._read(fh, gifExtenStruct.size))
|
||||
except:
|
||||
self.warnFlags = self.warnFlags | WARN_EOF | WARN_TRUNC
|
||||
return
|
||||
startOffset = fh.tell()
|
||||
|
||||
if extType == 0x01 and self.checkLevel >= CHECK_READ_ALL_TEXT: # Plain Text Block
|
||||
self._read(fh, gifPlaintextStruct.size)
|
||||
self.otherText = self.otherText or []
|
||||
blkSize = self._read(fh, 1)
|
||||
while blkSize and blkSize != '\x00':
|
||||
self.otherText.append(self._read(fh, ord(blkSize)))
|
||||
blkSize = self._read(fh, 1)
|
||||
elif extType == 0xFE and self.checkLevel >= CHECK_READ_COMMENTS: # Comment Block
|
||||
self.comments = self.comments or []
|
||||
blkSize = self._read(fh, 1)
|
||||
while blkSize and blkSize != '\x00':
|
||||
self.comments.append(self._read(fh, ord(blkSize)))
|
||||
blkSize = self._read(fh, 1)
|
||||
elif extType == 0xFF: # Application Block
|
||||
if blkSize == 0x0B and self._read(fh, blkSize) == "NETSCAPE2.0":
|
||||
try:
|
||||
a, b, self.loopCount = gifNetscapeStruct.unpack(self._read(fh, gifNetscapeStruct.size))
|
||||
except:
|
||||
self.warnFlags = self.warnFlags | WARN_EOF | WARN_TRUNC
|
||||
return
|
||||
|
||||
if a != 3 and b != 1:
|
||||
self.warnFlags = self.warnFlags | WARN_BAD_EXT
|
||||
|
||||
if not self.firstBlock:
|
||||
self.warnFlags = self.warnFlags | WARN_LOOP_POS
|
||||
else:
|
||||
fh.seek( startOffset + blkSize ) # Skip the contents
|
||||
|
||||
# Test for the block terminator
|
||||
if not self._skipSubBlocks(fh):
|
||||
self.warnFlags = self.warnFlags | WARN_BAD_EXT
|
||||
|
||||
def _getPalette(self, handle, bitfield):
|
||||
"""Using the size value from C{bitfield},
|
||||
load the palette at C{handle}'s current file pointer position."""
|
||||
if bitfield & int("10000000", 2):
|
||||
nBits = bitfield & int("00000111", 2)
|
||||
tableSize = 3 * 2**( nBits + 1 )
|
||||
return handle.read(tableSize)
|
||||
else:
|
||||
return ''
|
||||
|
||||
def _read(self, handle, size):
|
||||
"""Attempt to read the specified number of bytes. Set L{WARN_EOF} if
|
||||
fewer are received."""
|
||||
content = handle.read(size)
|
||||
if len(content) < size:
|
||||
self.warnFlags = self.warnFlags | WARN_EOF
|
||||
return content
|
||||
|
||||
def _skipSubBlocks(self, handle):
|
||||
"""Skip sub-blocks beginning at the current file pointer position
|
||||
using fseek."""
|
||||
offset = handle.tell()
|
||||
blkSize = handle.read(1)
|
||||
while blkSize and blkSize != '\x00':
|
||||
offset += ord(blkSize) + 1
|
||||
handle.seek(offset)
|
||||
blkSize = handle.read(1)
|
||||
return blkSize
|
||||
|
||||
_blockHandlers = {
|
||||
chr(0x2C) : _handleImageBlock,
|
||||
chr(0x21) : _handleGenericExtensionBlock,
|
||||
}
|
||||
|
||||
def gif_is_animated(path):
|
||||
"""A simple convenience function for testing whether a GIF is animated.
|
||||
@rtype: C{bool}
|
||||
"""
|
||||
return GifInfo(file(path,'rb'), CHECK_IS_ANIMATED).frameCount > 1
|
||||
|
||||
if __name__ == '__main__':
|
||||
from optparse import OptionParser
|
||||
parser = OptionParser(description=__doc__.split('\n\n')[0],
|
||||
version="%%prog v%s" % __version__, usage="%prog <path> ...")
|
||||
|
||||
opts, args = parser.parse_args()
|
||||
|
||||
if args:
|
||||
for fpath in args:
|
||||
try:
|
||||
info = GifInfo(fpath, CHECK_COUNT_FRAMES)
|
||||
warnFlags = (
|
||||
(info.warnFlags & WARN_BAD_IMG and 'I' or ' ') +
|
||||
(info.warnFlags & WARN_BAD_EXT and 'X' or ' ') +
|
||||
(info.warnFlags & WARN_BAD_SIZE and 'C' or ' ') +
|
||||
(info.warnFlags & WARN_BAD_BGCOLOR and 'B' or ' ') +
|
||||
(info.warnFlags & WARN_EOF and 'E' or ' ') +
|
||||
(info.warnFlags & WARN_TRUNC and 'T' or ' ') +
|
||||
(info.warnFlags & WARN_LOOP_POS and 'L' or ' ')
|
||||
)
|
||||
print "[%s](%3s Frames): %s" % (warnFlags, info.frameCount, info.path)
|
||||
except BadHeaderException, err:
|
||||
print "%s: %s" % (str(err), fpath)
|
||||
print "\nWarning Flags:"
|
||||
print " I = Image Chunk Corruption/Truncation"
|
||||
print " X = Extension Chunk Corruption/Truncation"
|
||||
print " C = Image Chunk Dimensions Exceed Global Canvas"
|
||||
print " B = Bad Background Color (Index Exceeds Palette Size)"
|
||||
print " E = Unexpected EOF Encountered (Missing Image Terminator)"
|
||||
print " T = EOF Encountered Within A Block Header (Corrupt or Truncated File)"
|
||||
print " L = Loop-control block misplaced within the file"
|
||||
print
|
||||
print "Note: A nearly-threefold speed-up can be had by using CHECK_IS_ANIMATED rather than CHECK_COUNT_FRAMES"
|
||||
+1
-1
@@ -36,7 +36,7 @@ if __name__=="__main__":
|
||||
|
||||
os.chdir('../included_dependencies')
|
||||
# 'a' for append
|
||||
files=['six.py','bs4','html5lib','chardet']
|
||||
files=['gif.py','six.py','bs4','html5lib','chardet']
|
||||
createZipFile("../"+filename,"a",
|
||||
files,
|
||||
exclude=exclude)
|
||||
|
||||
Reference in New Issue
Block a user