mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
merged changes from trunk
This commit is contained in:
commit
68ab64166d
@ -8,10 +8,16 @@ www.guardian.co.uk
|
|||||||
'''
|
'''
|
||||||
from calibre import strftime
|
from calibre import strftime
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
class Guardian(BasicNewsRecipe):
|
class Guardian(BasicNewsRecipe):
|
||||||
|
|
||||||
title = u'The Guardian'
|
title = u'The Guardian / The Observer'
|
||||||
|
if date.today().weekday() == 6:
|
||||||
|
base_url = "http://www.guardian.co.uk/theobserver"
|
||||||
|
else:
|
||||||
|
base_url = "http://www.guardian.co.uk/theguardian"
|
||||||
|
|
||||||
__author__ = 'Seabound and Sujata Raman'
|
__author__ = 'Seabound and Sujata Raman'
|
||||||
language = 'en_GB'
|
language = 'en_GB'
|
||||||
|
|
||||||
@ -19,6 +25,10 @@ class Guardian(BasicNewsRecipe):
|
|||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
|
|
||||||
|
# List of section titles to ignore
|
||||||
|
# For example: ['Sport']
|
||||||
|
ignore_sections = []
|
||||||
|
|
||||||
timefmt = ' [%a, %d %b %Y]'
|
timefmt = ' [%a, %d %b %Y]'
|
||||||
keep_only_tags = [
|
keep_only_tags = [
|
||||||
dict(name='div', attrs={'id':["content","article_header","main-article-info",]}),
|
dict(name='div', attrs={'id':["content","article_header","main-article-info",]}),
|
||||||
@ -28,6 +38,7 @@ class Guardian(BasicNewsRecipe):
|
|||||||
dict(name='div', attrs={'id':["article-toolbox","subscribe-feeds",]}),
|
dict(name='div', attrs={'id':["article-toolbox","subscribe-feeds",]}),
|
||||||
dict(name='ul', attrs={'class':["pagination"]}),
|
dict(name='ul', attrs={'class':["pagination"]}),
|
||||||
dict(name='ul', attrs={'id':["content-actions"]}),
|
dict(name='ul', attrs={'id':["content-actions"]}),
|
||||||
|
dict(name='img'),
|
||||||
]
|
]
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
|
|
||||||
@ -43,18 +54,6 @@ class Guardian(BasicNewsRecipe):
|
|||||||
#match-stats-summary{font-size:small; font-family:Arial,Helvetica,sans-serif;font-weight:normal;}
|
#match-stats-summary{font-size:small; font-family:Arial,Helvetica,sans-serif;font-weight:normal;}
|
||||||
'''
|
'''
|
||||||
|
|
||||||
feeds = [
|
|
||||||
('Front Page', 'http://www.guardian.co.uk/rss'),
|
|
||||||
('Business', 'http://www.guardian.co.uk/business/rss'),
|
|
||||||
('Sport', 'http://www.guardian.co.uk/sport/rss'),
|
|
||||||
('Culture', 'http://www.guardian.co.uk/culture/rss'),
|
|
||||||
('Money', 'http://www.guardian.co.uk/money/rss'),
|
|
||||||
('Life & Style', 'http://www.guardian.co.uk/lifeandstyle/rss'),
|
|
||||||
('Travel', 'http://www.guardian.co.uk/travel/rss'),
|
|
||||||
('Environment', 'http://www.guardian.co.uk/environment/rss'),
|
|
||||||
('Comment','http://www.guardian.co.uk/commentisfree/rss'),
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_article_url(self, article):
|
def get_article_url(self, article):
|
||||||
url = article.get('guid', None)
|
url = article.get('guid', None)
|
||||||
if '/video/' in url or '/flyer/' in url or '/quiz/' in url or \
|
if '/video/' in url or '/flyer/' in url or '/quiz/' in url or \
|
||||||
@ -76,7 +75,8 @@ class Guardian(BasicNewsRecipe):
|
|||||||
return soup
|
return soup
|
||||||
|
|
||||||
def find_sections(self):
|
def find_sections(self):
|
||||||
soup = self.index_to_soup('http://www.guardian.co.uk/theguardian')
|
# soup = self.index_to_soup("http://www.guardian.co.uk/theobserver")
|
||||||
|
soup = self.index_to_soup(self.base_url)
|
||||||
# find cover pic
|
# find cover pic
|
||||||
img = soup.find( 'img',attrs ={'alt':'Guardian digital edition'})
|
img = soup.find( 'img',attrs ={'alt':'Guardian digital edition'})
|
||||||
if img is not None:
|
if img is not None:
|
||||||
@ -113,13 +113,10 @@ class Guardian(BasicNewsRecipe):
|
|||||||
try:
|
try:
|
||||||
feeds = []
|
feeds = []
|
||||||
for title, href in self.find_sections():
|
for title, href in self.find_sections():
|
||||||
feeds.append((title, list(self.find_articles(href))))
|
if not title in self.ignore_sections:
|
||||||
|
feeds.append((title, list(self.find_articles(href))))
|
||||||
return feeds
|
return feeds
|
||||||
except:
|
except:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
def postprocess_html(self,soup,first):
|
|
||||||
return soup.findAll('html')[0]
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -404,14 +404,16 @@ class MetadataUpdater(object):
|
|||||||
if self.cover_record is not None:
|
if self.cover_record is not None:
|
||||||
size = len(self.cover_record)
|
size = len(self.cover_record)
|
||||||
cover = rescale_image(data, size)
|
cover = rescale_image(data, size)
|
||||||
cover += '\0' * (size - len(cover))
|
if len(cover) <= size:
|
||||||
self.cover_record[:] = cover
|
cover += '\0' * (size - len(cover))
|
||||||
|
self.cover_record[:] = cover
|
||||||
if self.thumbnail_record is not None:
|
if self.thumbnail_record is not None:
|
||||||
size = len(self.thumbnail_record)
|
size = len(self.thumbnail_record)
|
||||||
thumbnail = rescale_image(data, size, dimen=MAX_THUMB_DIMEN)
|
thumbnail = rescale_image(data, size, dimen=MAX_THUMB_DIMEN)
|
||||||
thumbnail += '\0' * (size - len(thumbnail))
|
if len(thumbnail) <= size:
|
||||||
self.thumbnail_record[:] = thumbnail
|
thumbnail += '\0' * (size - len(thumbnail))
|
||||||
return
|
self.thumbnail_record[:] = thumbnail
|
||||||
|
return
|
||||||
|
|
||||||
def set_metadata(stream, mi):
|
def set_metadata(stream, mi):
|
||||||
mu = MetadataUpdater(stream)
|
mu = MetadataUpdater(stream)
|
||||||
|
@ -112,15 +112,34 @@ def align_block(raw, multiple=4, pad='\0'):
|
|||||||
|
|
||||||
def rescale_image(data, maxsizeb, dimen=None):
|
def rescale_image(data, maxsizeb, dimen=None):
|
||||||
if dimen is not None:
|
if dimen is not None:
|
||||||
return thumbnail(data, width=dimen, height=dimen)[-1]
|
data = thumbnail(data, width=dimen, height=dimen)[-1]
|
||||||
# Replace transparent pixels with white pixels and convert to JPEG
|
else:
|
||||||
data = save_cover_data_to(data, 'img.jpg', return_data=True)
|
# Replace transparent pixels with white pixels and convert to JPEG
|
||||||
|
data = save_cover_data_to(data, 'img.jpg', return_data=True)
|
||||||
|
if len(data) <= maxsizeb:
|
||||||
|
return data
|
||||||
|
orig_data = data
|
||||||
|
img = Image()
|
||||||
|
quality = 95
|
||||||
|
|
||||||
|
if hasattr(img, 'set_compression_quality'):
|
||||||
|
img.load(data)
|
||||||
|
while len(data) >= maxsizeb and quality >= 10:
|
||||||
|
quality -= 5
|
||||||
|
img.set_compression_quality(quality)
|
||||||
|
data = img.export('jpg')
|
||||||
|
if len(data) <= maxsizeb:
|
||||||
|
return data
|
||||||
|
orig_data = data
|
||||||
|
|
||||||
scale = 0.9
|
scale = 0.9
|
||||||
while len(data) >= maxsizeb and scale >= 0.05:
|
while len(data) >= maxsizeb and scale >= 0.05:
|
||||||
img = Image()
|
img = Image()
|
||||||
img.load(data)
|
img.load(orig_data)
|
||||||
w, h = img.size
|
w, h = img.size
|
||||||
img.size = (int(scale*w), int(scale*h))
|
img.size = (int(scale*w), int(scale*h))
|
||||||
|
if hasattr(img, 'set_compression_quality'):
|
||||||
|
img.set_compression_quality(quality)
|
||||||
data = img.export('jpg')
|
data = img.export('jpg')
|
||||||
scale -= 0.05
|
scale -= 0.05
|
||||||
return data
|
return data
|
||||||
|
@ -308,7 +308,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
im = Image()
|
im = Image()
|
||||||
im.load(cdata)
|
im.load(cdata)
|
||||||
im.trim(10)
|
im.trim(10)
|
||||||
cdata = im.export('jpg')
|
cdata = im.export('png')
|
||||||
pix = QPixmap()
|
pix = QPixmap()
|
||||||
pix.loadFromData(cdata)
|
pix.loadFromData(cdata)
|
||||||
self.cover.setPixmap(pix)
|
self.cover.setPixmap(pix)
|
||||||
|
@ -490,26 +490,39 @@ class BooksView(QTableView): # {{{
|
|||||||
drag.setMimeData(md)
|
drag.setMimeData(md)
|
||||||
cover = self.drag_icon(m.cover(self.currentIndex().row()),
|
cover = self.drag_icon(m.cover(self.currentIndex().row()),
|
||||||
len(selected) > 1)
|
len(selected) > 1)
|
||||||
drag.setHotSpot(QPoint(cover.width()//3, cover.height()//3))
|
drag.setHotSpot(QPoint(-15, -15))
|
||||||
drag.setPixmap(cover)
|
drag.setPixmap(cover)
|
||||||
return drag
|
return drag
|
||||||
|
|
||||||
|
def event_has_mods(self, event=None):
|
||||||
|
mods = event.modifiers() if event is not None else \
|
||||||
|
QApplication.keyboardModifiers()
|
||||||
|
return mods & Qt.ControlModifier or mods & Qt.ShiftModifier
|
||||||
|
|
||||||
def mousePressEvent(self, event):
|
def mousePressEvent(self, event):
|
||||||
if event.button() == Qt.LeftButton:
|
if event.button() == Qt.LeftButton and not self.event_has_mods():
|
||||||
self.drag_start_pos = event.pos()
|
self.drag_start_pos = event.pos()
|
||||||
return QTableView.mousePressEvent(self, event)
|
return QTableView.mousePressEvent(self, event)
|
||||||
|
|
||||||
def mouseMoveEvent(self, event):
|
def mouseMoveEvent(self, event):
|
||||||
if not (event.buttons() & Qt.LeftButton) or self.drag_start_pos is None:
|
if self.drag_start_pos is None:
|
||||||
|
return QTableView.mouseMoveEvent(self, event)
|
||||||
|
|
||||||
|
if self.event_has_mods():
|
||||||
|
self.drag_start_pos = None
|
||||||
return
|
return
|
||||||
if (event.pos() - self.drag_start_pos).manhattanLength() \
|
|
||||||
< QApplication.startDragDistance():
|
if not (event.buttons() & Qt.LeftButton) or \
|
||||||
|
(event.pos() - self.drag_start_pos).manhattanLength() \
|
||||||
|
< QApplication.startDragDistance():
|
||||||
return
|
return
|
||||||
|
|
||||||
index = self.indexAt(event.pos())
|
index = self.indexAt(event.pos())
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return
|
return
|
||||||
drag = self.drag_data()
|
drag = self.drag_data()
|
||||||
drag.exec_(Qt.CopyAction)
|
drag.exec_(Qt.CopyAction)
|
||||||
|
self.drag_start_pos = None
|
||||||
|
|
||||||
def dragEnterEvent(self, event):
|
def dragEnterEvent(self, event):
|
||||||
if int(event.possibleActions() & Qt.CopyAction) + \
|
if int(event.possibleActions() & Qt.CopyAction) + \
|
||||||
@ -643,7 +656,7 @@ class DeviceBooksView(BooksView): # {{{
|
|||||||
drag.setMimeData(md)
|
drag.setMimeData(md)
|
||||||
cover = self.drag_icon(m.cover(self.currentIndex().row()), len(paths) >
|
cover = self.drag_icon(m.cover(self.currentIndex().row()), len(paths) >
|
||||||
1)
|
1)
|
||||||
drag.setHotSpot(QPoint(cover.width()//3, cover.height()//3))
|
drag.setHotSpot(QPoint(-15, -15))
|
||||||
drag.setPixmap(cover)
|
drag.setPixmap(cover)
|
||||||
return drag
|
return drag
|
||||||
|
|
||||||
|
@ -80,6 +80,7 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.setItemDelegate(TagDelegate(self))
|
self.setItemDelegate(TagDelegate(self))
|
||||||
self.made_connections = False
|
self.made_connections = False
|
||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
|
self.setDragDropMode(self.DropOnly)
|
||||||
self.setDropIndicatorShown(True)
|
self.setDropIndicatorShown(True)
|
||||||
|
|
||||||
def set_database(self, db, tag_match, sort_by):
|
def set_database(self, db, tag_match, sort_by):
|
||||||
|
@ -158,7 +158,7 @@ class Image(_magick.Image): # {{{
|
|||||||
format = ext[1:]
|
format = ext[1:]
|
||||||
format = format.upper()
|
format = format.upper()
|
||||||
|
|
||||||
with open(path, 'wb') as f:
|
with lopen(path, 'wb') as f:
|
||||||
f.write(self.export(format))
|
f.write(self.export(format))
|
||||||
|
|
||||||
def compose(self, img, left=0, top=0, operation='OverCompositeOp'):
|
def compose(self, img, left=0, top=0, operation='OverCompositeOp'):
|
||||||
|
@ -11,22 +11,57 @@ from calibre.utils.magick import Image, DrawingWand, create_canvas
|
|||||||
from calibre.constants import __appname__, __version__
|
from calibre.constants import __appname__, __version__
|
||||||
from calibre import fit_image
|
from calibre import fit_image
|
||||||
|
|
||||||
|
def normalize_format_name(fmt):
|
||||||
|
fmt = fmt.lower()
|
||||||
|
if fmt == 'jpeg':
|
||||||
|
fmt = 'jpg'
|
||||||
|
return fmt
|
||||||
|
|
||||||
def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None,
|
def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None,
|
||||||
return_data=False):
|
return_data=False, compression_quality=90):
|
||||||
'''
|
'''
|
||||||
Saves image in data to path, in the format specified by the path
|
Saves image in data to path, in the format specified by the path
|
||||||
extension. Composes the image onto a blank canvas so as to
|
extension. Removes any transparency. If there is no transparency and no
|
||||||
properly convert transparent images.
|
resize and the input and output image formats are the same, no changes are
|
||||||
|
made.
|
||||||
|
|
||||||
|
:param compression_quality: The quality of the image after compression.
|
||||||
|
Number between 1 and 100. 1 means highest compression, 100 means no
|
||||||
|
compression (lossless).
|
||||||
|
:param bgcolor: The color for transparent pixels. Must be specified in hex.
|
||||||
|
:param resize_to: A tuple (width, height) or None for no resizing
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
changed = False
|
||||||
img = Image()
|
img = Image()
|
||||||
img.load(data)
|
img.load(data)
|
||||||
|
orig_fmt = normalize_format_name(img.format)
|
||||||
|
fmt = os.path.splitext(path)[1]
|
||||||
|
fmt = normalize_format_name(fmt[1:])
|
||||||
|
|
||||||
if resize_to is not None:
|
if resize_to is not None:
|
||||||
img.size = (resize_to[0], resize_to[1])
|
img.size = (resize_to[0], resize_to[1])
|
||||||
canvas = create_canvas(img.size[0], img.size[1], bgcolor)
|
changed = True
|
||||||
canvas.compose(img)
|
if not hasattr(img, 'has_transparent_pixels') or img.has_transparent_pixels():
|
||||||
|
canvas = create_canvas(img.size[0], img.size[1], bgcolor)
|
||||||
|
canvas.compose(img)
|
||||||
|
img = canvas
|
||||||
|
changed = True
|
||||||
|
if not changed:
|
||||||
|
changed = fmt != orig_fmt
|
||||||
if return_data:
|
if return_data:
|
||||||
return canvas.export(os.path.splitext(path)[1][1:])
|
if changed:
|
||||||
canvas.save(path)
|
if hasattr(img, 'set_compression_quality') and fmt == 'jpg':
|
||||||
|
img.set_compression_quality(compression_quality)
|
||||||
|
return img.export(fmt)
|
||||||
|
return data
|
||||||
|
if changed:
|
||||||
|
if hasattr(img, 'set_compression_quality') and fmt == 'jpg':
|
||||||
|
img.set_compression_quality(compression_quality)
|
||||||
|
img.save(path)
|
||||||
|
else:
|
||||||
|
with lopen(path, 'wb') as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg'):
|
def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg'):
|
||||||
img = Image()
|
img = Image()
|
||||||
@ -37,6 +72,8 @@ def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg'):
|
|||||||
img.size = (nwidth, nheight)
|
img.size = (nwidth, nheight)
|
||||||
canvas = create_canvas(img.size[0], img.size[1], bgcolor)
|
canvas = create_canvas(img.size[0], img.size[1], bgcolor)
|
||||||
canvas.compose(img)
|
canvas.compose(img)
|
||||||
|
if fmt == 'jpg' and hasattr(canvas, 'set_compression_quality'):
|
||||||
|
canvas.set_compression_quality(70)
|
||||||
return (canvas.size[0], canvas.size[1], canvas.export(fmt))
|
return (canvas.size[0], canvas.size[1], canvas.export(fmt))
|
||||||
|
|
||||||
def identify_data(data):
|
def identify_data(data):
|
||||||
|
@ -725,6 +725,49 @@ magick_Image_set_page(magick_Image *self, PyObject *args, PyObject *kwargs) {
|
|||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
|
||||||
|
// Image.set_compression_quality {{{
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
magick_Image_set_compression_quality(magick_Image *self, PyObject *args, PyObject *kwargs) {
|
||||||
|
Py_ssize_t quality;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTuple(args, "n", &quality)) return NULL;
|
||||||
|
|
||||||
|
if (!MagickSetImageCompressionQuality(self->wand, quality)) return magick_set_exception(self->wand);
|
||||||
|
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
// Image.has_transparent_pixels {{{
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
magick_Image_has_transparent_pixels(magick_Image *self, PyObject *args, PyObject *kwargs) {
|
||||||
|
PixelIterator *pi = NULL;
|
||||||
|
PixelWand **pixels = NULL;
|
||||||
|
int found = 0;
|
||||||
|
size_t r, c, width, height;
|
||||||
|
double alpha;
|
||||||
|
|
||||||
|
height = MagickGetImageHeight(self->wand);
|
||||||
|
pi = NewPixelIterator(self->wand);
|
||||||
|
|
||||||
|
for (r = 0; r < height; r++) {
|
||||||
|
pixels = PixelGetNextIteratorRow(pi, &width);
|
||||||
|
for (c = 0; c < width; c++) {
|
||||||
|
alpha = PixelGetAlpha(pixels[c]);
|
||||||
|
if (alpha < 1.00) {
|
||||||
|
found = 1;
|
||||||
|
c = width; r = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pi = DestroyPixelIterator(pi);
|
||||||
|
if (found) Py_RETURN_TRUE;
|
||||||
|
Py_RETURN_FALSE;
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
||||||
// Image.normalize {{{
|
// Image.normalize {{{
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
@ -872,6 +915,14 @@ static PyMethodDef magick_Image_methods[] = {
|
|||||||
"set_page(width, height, x, y) \n\n Sets the page geometry of the image."
|
"set_page(width, height, x, y) \n\n Sets the page geometry of the image."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{"set_compression_quality", (PyCFunction)magick_Image_set_compression_quality, METH_VARARGS,
|
||||||
|
"set_compression_quality(quality) \n\n Sets the compression quality when exporting the image."
|
||||||
|
},
|
||||||
|
|
||||||
|
{"has_transparent_pixels", (PyCFunction)magick_Image_has_transparent_pixels, METH_VARARGS,
|
||||||
|
"has_transparent_pixels() \n\n Returns True iff image has a (semi-) transparent pixel"
|
||||||
|
},
|
||||||
|
|
||||||
{"thumbnail", (PyCFunction)magick_Image_thumbnail, METH_VARARGS,
|
{"thumbnail", (PyCFunction)magick_Image_thumbnail, METH_VARARGS,
|
||||||
"thumbnail(width, height) \n\n Convert to a thumbnail of specified size."
|
"thumbnail(width, height) \n\n Convert to a thumbnail of specified size."
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user