def distributed_collage(self, callback, orientation, alignment, urls): logger.debug('filters.distributed_collage: distributed_collage invoked') self.storage = self.context.modules.storage self.callback = callback self.orientation = orientation self.alignment = alignment self.urls = urls.split('|') self.images = {} total = len(self.urls) if total > self.MAX_IMAGES: logger.error('filters.distributed_collage: Too many images to join') callback() elif total == 0: logger.error('filters.distributed_collage: No images to join') callback() else: self.urls = self.urls[:self.MAX_IMAGES] for url in self.urls: self.images[url] = Picture(url, self) # second loop needed to ensure that all images are in self.images # otherwise, self.on_image_fetch can call the self.assembly() # without that all images had being loaded for url in self.urls: buffer = yield tornado.gen.maybe_future(self.storage.get(url)) pic = self.images[url] if buffer is not None: pic.fill_buffer(buffer) self.on_image_fetch() else: pic.request()
def dispatch(self, file_key): """ Callback method for getObject from s3 """ if not file_key or 'Error' in file_key or 'Body' not in file_key: logger.error( "ERROR retrieving image from S3 {0}: {1}". format(self.key, str(file_key))) # If we got here, there was a failure. # We will return 404 if S3 returned a 404, otherwise 502. result = LoaderResult() result.successful = False if not file_key: result.error = LoaderResult.ERROR_UPSTREAM self.callback(result) return response_metadata = file_key.get('ResponseMetadata', {}) status_code = response_metadata.get('HTTPStatusCode') if status_code == 404: result.error = LoaderResult.ERROR_NOT_FOUND self.callback(result) return if self.retries_counter < self.max_retry: self.__increment_retry_counter() self.bucket_loader.get(self.key, callback=self.dispatch) else: result.error = LoaderResult.ERROR_UPSTREAM self.callback(result) else: self.callback(file_key['Body'].read())
def load(self, buffer, extension): self.extension = extension if extension is None: mime = self.get_mimetype(buffer) self.extension = EXTENSION.get(mime, '.jpg') if self.extension == '.svg': buffer = self.convert_svg_to_png(buffer) image_or_frames = self.create_image(buffer) if METADATA_AVAILABLE: try: self.metadata = ImageMetadata.from_buffer(buffer) self.metadata.read() except Exception as e: logger.error('Error reading image metadata: %s' % e) if self.context.config.ALLOW_ANIMATED_GIFS and isinstance( image_or_frames, (list, tuple)): self.image = image_or_frames[0] if len(image_or_frames) > 1: self.multiple_engine = MultipleEngine(self) for frame in image_or_frames: self.multiple_engine.add_frame(frame) self.wrap(self.multiple_engine) else: self.image = image_or_frames if self.source_width is None: self.source_width = self.size[0] if self.source_height is None: self.source_height = self.size[1]
def command( cls, context, pre=[], post=[], buffer='', input_temp_file=None ): if not input_temp_file: input_temp_file = NamedTemporaryFile() input_temp_file.write(buffer) input_temp_file.flush() command = [context.config.EXIFTOOL_PATH] command += pre command.append(input_temp_file.name) command += post logger.debug('[ExiftoolRunner] command: %r' % command, extra=log_extra(context)) code, stderr, stdout = ShellRunner.command(command, context) input_temp_file.close() if stderr: logger.error('[ExiftoolRunner] error: %r' % stderr, extra=log_extra(context)) return stdout
def post(self, **kwargs): self.should_return_image = False # URL can be passed as a URL argument or in the body url = kwargs['url'] if 'url' in kwargs else kwargs['key'] if not url: logger.error("Couldn't find url param in body or key in URL...") raise tornado.web.HTTPError(404) options = RequestParser.path_to_parameters(url) yield self.check_image(options) # We check the status code, if != 200 the image is incorrect, and we shouldn't store the key if self.get_status() == 200: logger.debug("Image is checked, clearing the response before trying to store...") self.clear() try: shortener = Shortener(self.context) key = shortener.generate(url) shortener.put(key, url) self.write(json.dumps({'key': key})) self.set_header("Content-Type", "application/json") except Exception as e: logger.error("An error occurred while trying to store shortened URL: {error}.".format(error=e.message)) self.set_status(500) self.write(json.dumps({'error': e.message}))
def get_image(self): try: result = yield self._fetch(self.context.request.image_url) if not result.successful: if result.loader_error == LoaderResult.ERROR_NOT_FOUND: self._error(404) return elif result.loader_error == LoaderResult.ERROR_UPSTREAM: # Return a Bad Gateway status if the error came from upstream self._error(502) return elif result.loader_error == LoaderResult.ERROR_TIMEOUT: # Return a Gateway Timeout status if upstream timed out (i.e. 599) self._error(504) return else: self._error(500) return except Exception as e: msg = "[BaseHandler] get_image failed for url `{url}`. error: `{error}`".format( url=self.context.request.image_url, error=e ) self.log_exception(*sys.exc_info()) if "cannot identify image file" in e.message: logger.warning(msg) self._error(400) else: logger.error(msg) self._error(500) return normalized = result.normalized buffer = result.buffer engine = result.engine req = self.context.request if engine is None: if buffer is None: self._error(504) return engine = self.context.request.engine engine.load(buffer, self.context.request.extension) def transform(): self.normalize_crops(normalized, req, engine) if req.meta: self.context.request.engine = JSONEngine(engine, req.image_url, req.meta_callback) after_transform_cb = functools.partial(self.after_transform, self.context) Transformer(self.context).transform(after_transform_cb) self.filters_runner.apply_filters(thumbor.filters.PHASE_AFTER_LOAD, transform)
def __init__(self, context): super(Optimizer, self).__init__(context) self.runnable = True self.pngcrush_path = self.context.config.PNGCRUSH_PATH if not (os.path.isfile(self.pngcrush_path) and os.access(self.pngcrush_path, os.X_OK)): logger.error("ERROR pngcrush path '{0}' is not accessible".format(self.pngcrush_path)) self.runnable = False
def __init__(self, context): super(Optimizer, self).__init__(context) self.runnable = True self.imgmin_path = self.context.config.IMGMIN_PATH if not ( os.path.isfile(self.imgmin_path) ): logger.error("ERROR path '{0}' is not accessible".format(self.imgmin_path)) self.runnable = False
def __init__(self, context): super(Optimizer, self).__init__(context) self.runnable = True self.zopflipng_path = self.context.config.ZOPFLIPNG_PATH if not (os.path.isfile(self.zopflipng_path) and os.access(self.zopflipng_path, os.X_OK)): logger.error("ERROR zopflipng path '{0}' is not accessible".format(self.zopflipng_path)) self.runnable = False
def return_contents(response, url, callback): if response.error: logger.error("ERROR retrieving image {0}: {1}".format(url, str(response.error))) callback(None) elif response.body is None or len(response.body) == 0: logger.error("ERROR retrieving image {0}: Empty response.".format(url)) callback(None) else: callback(response.body)
def __init__(self, context): super(Optimizer, self).__init__(context) self.runnable = True self.jpegrecompress_path = self.context.config.JPEGRECOMPRESS_PATH if not (os.path.isfile(self.jpegrecompress_path) and os.access(self.jpegrecompress_path, os.X_OK)): logger.error("ERROR jpeg-recompress path '{0}' is not accessible".format(self.jpegrecompress_path)) self.runnable = False
def __init__(self, context): super(Optimizer, self).__init__(context) self.runnable = True self.mozjpeg_path = self.context.config.MOZJPEG_PATH self.mozjpeg_level = self.context.config.MOZJPEG_QUALITY or '75' if not (os.path.isfile(self.mozjpeg_path) and os.access(self.mozjpeg_path, os.X_OK)): logger.error("ERROR mozjpeg path '{0}' is not accessible".format(self.mozjpeg_path)) self.runnable = False
def put(self, bytes): normalized_path = self.normalize_path(self.context.request.url) uri = self.context.config.get('RESULT_STORAGE_WEBDAV_URI') + normalized_path logger.debug("[RESULT_STORAGE] Making PUT request to: %s", uri) http_client = HTTPClient() try: response = http_client.fetch(uri, method='PUT', body=bytes) logger.debug("[RESULT_STORAGE] Success on PUT request!") except HTTPError as e: logger.error("[RESULT_STORAGE] Error on PUT request: %s", e)
def handle_error(self, context, handler, exception): ex_type, value, tb = exception ex_msg = traceback.format_exception_only(ex_type, value) tb_msg = traceback.format_tb(tb) extra = log_extra(context) extra['traceback'] = ''.join(tb_msg) logger.error(''.join(ex_msg), extra=extra)
def validate(self, path): if not hasattr(self.loader, 'validate'): return True is_valid = self.loader.validate(path) if not is_valid: logger.error('Request denied because the specified path "%s" was not identified by the loader as a valid path' % path) return is_valid
def __init__(self, context): super(Optimizer, self).__init__(context) self.runnable = True self.pngquant_path = self.context.config.PNGQUANT_PATH self.pngquant_quality = self.context.config.PNGQUANT_QUALITY or '65-80' self.pngquant_speed = self.context.config.PNGQUANT_SPEED or '3' if not (os.path.isfile(self.pngquant_path) and os.access(self.pngquant_path, os.X_OK)): logger.error("ERROR pnqquant path '{0}' is not accessible".format(self.pngquant_path)) self.runnable = False
def _handle_request_exception(self, e): try: exc_info = sys.exc_info() msg = traceback.format_exception(exc_info[0], exc_info[1], exc_info[2]) if self.context.config.USE_CUSTOM_ERROR_HANDLING: self.context.modules.importer.error_handler.handle_error(context=self.context, handler=self, exception=exc_info) finally: del exc_info logger.error('ERROR: %s' % "".join(msg)) self.send_error(500)
def convert_svg_to_png(self, buffer): if not cairosvg: msg = """[BaseEngine] convert_svg_to_png failed cairosvg not imported (if you want svg conversion to png please install cairosvg) """ logger.error(msg) return buffer buffer = cairosvg.svg2png(bytestring=buffer, dpi=self.context.config.SVG_DPI) mime = self.get_mimetype(buffer) self.extension = EXTENSION.get(mime, '.jpg') return buffer
def convert_tif_to_png(self, buffer): if not numpy: msg = """[BaseEngin] convert_tif_to_png failed numpy not imported""" logger.error(msg) return buffer if not cv2: msg = """[BaseEngin] convert_tif_to_png failed opencv not imported""" logger.error(msg) return buffer img = cv2.imdecode(numpy.fromstring(buffer), -1) img_str = cv2.imencode('.png', img)[1].tostring() return img_str
def log_exception(self, *exc_info): if isinstance(exc_info[1], tornado.web.HTTPError): # Delegate HTTPError's to the base class # We don't want these through normal exception handling return super(ContextHandler, self).log_exception(*exc_info) msg = traceback.format_exception(*exc_info) try: if self.context.config.USE_CUSTOM_ERROR_HANDLING: self.context.modules.importer.error_handler.handle_error(context=self.context, handler=self, exception=exc_info) finally: del exc_info logger.error('ERROR: %s' % "".join(msg))
def _read(self, update_image=True): buffer = self.flush_operations(update_image) # Make sure gifsicle produced a valid gif. try: with BytesIO(buffer) as buff: Image.open(buff).verify() except Exception: self.context.metrics.incr('gif_engine.no_output') logger.error("[GIF_ENGINE] invalid gif engine result for url `{url}`.".format( url=self.context.request.url )) raise return buffer
def read(self, extension=None, quality=None): self.flush_operations() # Make sure gifsicle produced a valid gif. try: with Image.open(BytesIO(self.buffer)) as image: image.verify() except Exception: self.context.statsd_client.incr('gif_engine.no_output') logger.error("[GIF_ENGINE] invalid gif engine result for url `{url}`.".format( url=self.context.request.url )) raise return self.buffer
def convert_tif_to_png(self, buffer): if not cv2: msg = """[PILEngine] convert_tif_to_png failed: opencv not imported""" logger.error(msg) return buffer if not numpy: msg = """[PILEngine] convert_tif_to_png failed: opencv not imported""" logger.error(msg) return buffer img = cv2.imdecode(numpy.fromstring(buffer, dtype='uint16'), -1) buffer = cv2.imencode('.png', img)[1].tostring() mime = self.get_mimetype(buffer) self.extension = EXTENSION.get(mime, '.jpg') return buffer
def detect(self, callback): self.context.request.prevent_result_storage = True try: if not self.pyremotecv: self.pyremotecv = PyRemoteCV(host=self.context.config.REDIS_QUEUE_SERVER_HOST, port=self.context.config.REDIS_QUEUE_SERVER_PORT, db=self.context.config.REDIS_QUEUE_SERVER_DB, password=self.context.config.REDIS_QUEUE_SERVER_PASSWORD) self.pyremotecv.async_detect('remotecv.pyres_tasks.DetectTask', 'Detect', args=[self.detection_type, self.context.request.image_url], key=self.context.request.image_url) except RedisError: self.context.request.detection_error = True logger.error(traceback.format_exc()) finally: callback([])
def save_on_disc(self): if self.fetched: try: self.engine.load(self.buffer, self.extension) except Exception as err: self.failed = True logger.exception(err) try: self.thumbor_filter.storage.put(self.url, self.engine.read()) self.thumbor_filter.storage.put_crypto(self.url) except Exception as err: self.failed = True logger.exception(err) else: self.failed = True logger.error("filters.distributed_collage: Can't save unfetched image")
def convert_tif_to_png(self, buffer): if not cv: msg = """[PILEngine] convert_tif_to_png failed: opencv not imported""" logger.error(msg) return buffer # can not use cv2 here, because ubuntu precise shipped with python-opencv 2.3 which has bug with imencode # requires 3rd parameter buf which could not be created in python. Could be replaced with these lines: # img = cv2.imdecode(numpy.fromstring(buffer, dtype='uint16'), -1) # buffer = cv2.imencode('.png', img)[1].tostring() mat_data = cv.CreateMatHeader(1, len(buffer), cv.CV_8UC1) cv.SetData(mat_data, buffer, len(buffer)) img = cv.DecodeImage(mat_data, -1) buffer = cv.EncodeImage(".png", img).tostring() mime = self.get_mimetype(buffer) self.extension = EXTENSION.get(mime, '.jpg') return buffer
def __init__(self, context): super(Optimizer, self).__init__(context) self.runnable = True self.imgmin_path = self.context.config.IMGMIN_PATH # All defaults are from imgmin unless noted otherwise self.error_threshold = self.context.config.AUTO_ERROR_THRESHOLD or "1.0" self.color_density_ratio = self.context.config.AUTO_COLOR_DENSITY_RATIO or "0.11" self.min_unique_colors = self.context.config.AUTO_MIN_UNIQUE_COLORS or "4096" self.quality_out_max = self.context.config.AUTO_QUALITY_OUT_MAX or "100" # imgming default 95 self.quality_out_min = self.context.config.AUTO_QUALITY_OUT_MIN or "95" # imgming default 70 self.quality_in_min = self.context.config.AUTO_QUALITY_IN_MIN or "82" self.max_steps = self.context.config.AUTO_MAX_STEPS or "5" if not (os.path.isfile(self.imgmin_path)): logger.error("ERROR path '{0}' or '{1}' is not accessible".format(self.imgmin_path)) self.runnable = False
def run_gifsicle(self, command): p = Popen([self.context.server.gifsicle_path] + command.split(' '), stdout=PIPE, stdin=PIPE, stderr=PIPE) stdout_data, stderr_data = p.communicate(input=self.buffer) if p.returncode != 0: logger.error(stderr_data) if stdout_data is None: raise GifSicleError( 'gifsicle command returned errorlevel {0} for command "{1}" (image maybe corrupted?)'.format( p.returncode, ' '.join( [self.context.server.gifsicle_path] + command.split(' ') + [self.context.request.url] ) ) ) return stdout_data
def normalize_color_to_hex(self, color_string): try: return webcolors.normalize_hex("#" + color_string) except ValueError: pass try: return webcolors.name_to_hex(color_string) except ValueError: pass try: return webcolors.normalize_hex(color_string) except ValueError: pass if color_string: logger.error('background_color value could not be parsed')
def put(self, path, bytes): start = datetime.datetime.now() normalized_path = self._normalize_path(path) content_type = 'text/plain' if bytes: try: mime = BaseEngine.get_mimetype(bytes) content_type = mime except: logger.error("[GoogleCloudStorage] Couldn't determine mimetype for %s" % path) blob = self._get_bucket().blob(normalized_path) blob.upload_from_string(bytes, content_type=content_type) finish = datetime.datetime.now() self.context.metrics.timing('gcs.put.{0}'.format(normalized_path), (finish - start).total_seconds() * 1000)
def _process_done( callback, process, context, normalized_url, seek, output_file, status ): # T183907 Sometimes ffmpeg returns status 0 and actually fails to # generate a thumbnail. We double-check the existence of the thumbnail # in case of apparent success if status == 0: if os.stat(output_file.name).st_size == 0: status = -1 # If rendering the desired frame fails, attempt to render the # first frame instead if status != 0 and seek > 0: seek_and_screenshot(callback, context, normalized_url, 0) return result = LoaderResult() if status != 0: # pragma: no cover _http_code_from_stderr(context, process, result, normalized_url) else: result.successful = True result.buffer = output_file.read() process.stdout.close() process.stderr.close() output_file.close() try: os.unlink(output_file.name) except OSError as e: # pragma: no cover if e.errno != errno.ENOENT: logger.error('[Video] Unable to unlink output file', extra=log_extra(context)) raise callback(result)
def on_redis_error(self, fname, exc_type, exc_value): '''Callback executed when there is a redis error. :param string fname: Function name that was being called. :param type exc_type: Exception type :param Exception exc_value: The current exception :returns: Default value or raise the current exception ''' if self.shared_client: Storage.storage = None else: self.storage = None if self.context.config.REDIS_RESULT_STORAGE_IGNORE_ERRORS is True: logger.error("Redis result storage failure: %s" % exc_value) return None else: raise exc_value
def reorientate(self, override_exif=True): """ Rotates the image in the buffer so that it is oriented correctly. If override_exif is True (default) then the metadata orientation is adjusted as well. :param override_exif: If the metadata should be adjusted as well. :type override_exif: Boolean """ exif = self._get_exif_object() if exif is None: return None orientation = exif.get_orientation() if orientation is None: return if orientation == 2: self.flip_horizontally() elif orientation == 3: self.rotate(180) elif orientation == 4: self.flip_vertically() elif orientation == 5: # Horizontal Mirror + Rotation 270 CCW self.flip_vertically() self.rotate(270) elif orientation == 6: self.rotate(270) elif orientation == 7: # Vertical Mirror + Rotation 270 CCW self.flip_horizontally() self.rotate(270) elif orientation == 8: self.rotate(90) if orientation != 1 and override_exif: try: exif.set_orientation(1) self.exif = exif.tobytes() except Exception as e: msg = """[exif] %s""" % e logger.error(msg)
def load(context, path, callback): result = LoaderResult() for idx, next_dir in enumerate(context.config.TC_MULTIDIR_PATHS): file_path = join(next_dir.rstrip('/'), path.lstrip('/')) file_path = abspath(file_path) inside_root_path = file_path.startswith(abspath(next_dir)) if inside_root_path: # keep backwards compatibility, try the actual path first # if not found, unquote it and try again found = exists(file_path) if not found: file_path = unquote(file_path) found = exists(file_path) if found: with open(file_path, 'rb') as f: stats = fstat(f.fileno()) result.successful = True result.buffer = f.read() result.metadata.update( size=stats.st_size, updated_at=datetime.utcfromtimestamp(stats.st_mtime)) callback(result) return logger.debug('TC_MULTIDIR: File {0} not found in {1}'.format(path, next_dir)) # else loop and try next directory if not context.config.TC_MULTIDIR_PATHS: logger.error('TC_MULTIDIR: No paths set in configuration TC_MULTIDIR_PATHS') # no file found result.error = LoaderResult.ERROR_NOT_FOUND result.successful = False callback(result)
def convert_svg_to_png(self, buffer): if not cairosvg: msg = """[BaseEngine] convert_svg_to_png failed cairosvg not imported (if you want svg conversion to png please install cairosvg) """ logger.error(msg) return buffer try: buffer = cairosvg.svg2png(bytestring=buffer, dpi=self.context.config.SVG_DPI) mime = self.get_mimetype(buffer) self.extension = EXTENSION.get(mime, '.jpg') except ParseError: mime = self.get_mimetype(buffer) extension = EXTENSION.get(mime) if extension is None or extension == '.svg': raise self.extension = extension return buffer
def detect(self, callback): self.context.request.prevent_result_storage = True try: if not self.pyremotecv: self.pyremotecv = PyRemoteCV( host=self.context.config.REDIS_QUEUE_SERVER_HOST, port=self.context.config.REDIS_QUEUE_SERVER_PORT, db=self.context.config.REDIS_QUEUE_SERVER_DB, password=self.context.config.REDIS_QUEUE_SERVER_PASSWORD) self.pyremotecv.async_detect( 'remotecv.pyres_tasks.DetectTask', 'Detect', args=[self.detection_type, self.context.request.image_url], key=self.context.request.image_url) except RedisError: self.context.request.detection_error = True logger.error(traceback.format_exc()) finally: callback([])
def reorientate(self, override_exif=True): """ Rotates the image in the buffer so that it is oriented correctly. If override_exif is True (default) then the metadata orientation is adjusted as well. :param override_exif: If the metadata should be adjusted as well. :type override_exif: Boolean """ orientation = self.get_orientation() if orientation is None: return if orientation == 2: self.flip_horizontally() elif orientation == 3: self.rotate(180) elif orientation == 4: self.flip_vertically() elif orientation == 5: # Horizontal Mirror + Rotation 270 CCW self.flip_vertically() self.rotate(270) elif orientation == 6: self.rotate(270) elif orientation == 7: # Vertical Mirror + Rotation 270 CCW self.flip_horizontally() self.rotate(270) elif orientation == 8: self.rotate(90) if orientation != 1 and override_exif: exif_dict = self._get_exif_segment() if exif_dict and piexif.ImageIFD.Orientation in exif_dict["0th"]: exif_dict["0th"][piexif.ImageIFD.Orientation] = 1 try: self.exif = piexif.dump(exif_dict) except Exception as e: msg = """[piexif] %s""" % e logger.error(msg)
def get_image(self): try: normalized, buffer, engine = yield self._fetch( self.context.request.image_url) except Exception as e: msg = '[BaseHandler] get_image failed for url `{url}`. error: `{error}`'.format( url=self.context.request.image_url, error=e) self.log_exception(*sys.exc_info()) if 'cannot identify image file' in e.message: logger.warning(msg) self._error(400) else: logger.error(msg) self._error(500) return req = self.context.request if engine is None: if buffer is None: self._error(504) return engine = self.context.request.engine engine.load(buffer, self.context.request.extension) def transform(): self.normalize_crops(normalized, req, engine) if req.meta: self.context.request.engine = JSONEngine( engine, req.image_url, req.meta_callback) after_transform_cb = functools.partial(self.after_transform, self.context) Transformer(self.context).transform(after_transform_cb) self.filters_runner.apply_filters(thumbor.filters.PHASE_AFTER_LOAD, transform)
def run_gifsicle(self, command): process = Popen( [self.context.server.gifsicle_path] + command.split(" "), stdout=PIPE, stdin=PIPE, stderr=PIPE, ) stdout_data, stderr_data = process.communicate(input=self.buffer) if process.returncode != 0: logger.error(stderr_data) if stdout_data is None: raise GifSicleError( ("gifsicle command returned errorlevel {0} for " 'command "{1}" (image maybe corrupted?)').format( process.returncode, " ".join([self.context.server.gifsicle_path] + command.split(" ") + [self.context.request.url]), )) return stdout_data
def _get(self, key, col): try: key = md5(key).hexdigest() + '-' + key except UnicodeEncodeError: key = md5( key.encode('utf-8')).hexdigest() + '-' + key.encode('utf-8') if self.storage is None: self._connect() try: r = self.storage.get(self.table, key, self.data_fam + ':' + col)[0] except IndexError: r = None except: r = None logger.error("Error retrieving image from HBase; key " + key) self.hbase_server_offset = self.hbase_server_offset + 1 return r
def load(self, buffer, extension): self.extension = extension if extension is None: mime = self.get_mimetype(buffer) self.extension = EXTENSION.get(mime, '.jpg') if self.extension == '.svg': buffer = self.convert_svg_to_png(buffer) # added by zhaorong, if it is a tif picture, convert it to png if self.extension == '.tif': buffer = self.convert_tif_to_png(buffer) image_or_frames = self.create_image(buffer) if image_or_frames is None: return if METADATA_AVAILABLE: try: self.metadata = ImageMetadata.from_buffer(buffer) self.metadata.read() except Exception as e: logger.error('Error reading image metadata: %s' % e) if self.context.config.ALLOW_ANIMATED_GIFS and isinstance( image_or_frames, (list, tuple)): self.image = image_or_frames[0] if len(image_or_frames) > 1: self.multiple_engine = MultipleEngine(self) for frame in image_or_frames: self.multiple_engine.add_frame(frame) self.wrap(self.multiple_engine) else: self.image = image_or_frames if self.source_width is None: self.source_width = self.size[0] if self.source_height is None: self.source_height = self.size[1]
def post(self, **kwargs): self.should_return_image = False content_type = self.request.headers.get("Content-Type", '') if 'key' in kwargs and kwargs['key']: url = kwargs['key'] elif content_type.startswith("application/json"): data = json.loads(self.request.body) url = data['url'] if 'url' in data else None else: url = self.get_body_argument('url', None) if not url: logger.error("Couldn't find url param in body or key in URL...") raise tornado.web.HTTPError(400) options = RequestParser.path_to_parameters(url) yield self.check_image(options) # We check the status code, if != 200 the image is incorrect, and we shouldn't store the key if self.get_status() == 200: logger.debug( "Image is checked, clearing the response before trying to store..." ) self.clear() try: shortener = Shortener(self.context) key = shortener.generate(url) shortener.put(key, url) self.write(json.dumps({'key': key})) self.set_header("Content-Type", "application/json") except Exception as e: logger.error( "An error occurred while trying to store shortened URL: {error}." .format(error=e.message)) self.set_status(500) self.write(json.dumps({'error': e.message}))
def put(self, path, bytes): file_abspath = self._normalize_path(path) logger.debug("[STORAGE] putting at %s" % file_abspath) bucket = self._get_bucket() blob = bucket.blob(file_abspath) blob.upload_from_string(bytes) max_age = self.context.config.CLOUD_STORAGE_MAX_AGE blob.cache_control = "public,max-age=%s" % max_age if bytes: try: mime = BaseEngine.get_mimetype(bytes) blob.content_type = mime except Exception as ex: logger.debug("[STORAGE] Couldn't determine mimetype: %s" % ex) try: blob.patch() except Exception as ex: logger.error("[STORAGE] Couldn't patch blob: %s" % ex)
async def distributed_collage(self, orientation, alignment, urls): self.orientation = orientation self.alignment = alignment self.urls = urls.split("|") self.images = {} total = len(self.urls) if total > self.MAX_IMAGES: logger.error("filters.distributed_collage: Too many images to join") return elif total == 0: logger.error("filters.distributed_collage: No images to join") return else: self.urls = self.urls[: self.MAX_IMAGES] self.max_age = self.context.config.MAX_AGE self._calculate_dimensions() await self._fetch_images() self.context.request.max_age = self.max_age
def load(self, buffer, extension): self.extension = extension if extension is None: mime = self.get_mimetype(buffer) self.extension = EXTENSION.get(mime, ".jpg") if self.extension == ".svg": buffer = self.convert_svg_to_png(buffer) image_or_frames = self.create_image(buffer) if image_or_frames is None: return if METADATA_AVAILABLE: try: self.metadata = ImageMetadata.from_buffer(buffer) self.metadata.read() except Exception as error: # pylint: disable=broad-except logger.error("Error reading image metadata: %s", error) if self.context.config.ALLOW_ANIMATED_GIFS and isinstance( image_or_frames, (list, tuple) ): self.image = image_or_frames[0] if len(image_or_frames) > 1: self.multiple_engine = MultipleEngine(self) for frame in image_or_frames: self.multiple_engine.add_frame(frame) self.wrap(self.multiple_engine) else: self.image = image_or_frames if self.source_width is None: self.source_width = self.size[0] if self.source_height is None: self.source_height = self.size[1]
def get_pdf_page(context, file_path): """ A context manager that extracts a single page out of a pdf file and stores it in a temporary file. Returns the path of the temporary file or None in case of failure. """ import subprocess import tempfile import os gspath = '/usr/bin/gs' # Fail nicely when ffmpeg cannot be found if not os.path.exists(gspath): logger.error('%s does not exist, please configure Ghostscript', gspath) yield None return # Prepare temporary file f, image_path = tempfile.mkstemp('.jpg') os.close(f) # Extract image try: cmd = [ gspath, '-sDEVICE=jpeg', '-dDOINTERPOLATE', '-dCOLORSCREEN', '-dFirstPage=1', '-dLastPage=1', '-dPDFFitPage', '-dBATCH', '-dJPEGQ=85', '-r120x120', '-dNOPAUSE', '-sOutputFile="' + image_path + '"', file_path ] subprocess.check_call(cmd) yield image_path except: logger.exception('Cannot extract image frame from %s', file_path) yield None finally: # Cleanup try_to_delete(image_path)
def distributed_collage(self, callback, orientation, alignment, urls): self.callback = callback self.orientation = orientation self.alignment = alignment self.urls = urls.split('|') self.images = {} total = len(self.urls) if total > self.MAX_IMAGES: logger.error( 'filters.distributed_collage: Too many images to join') callback() elif total == 0: logger.error('filters.distributed_collage: No images to join') callback() else: self.urls = self.urls[:self.MAX_IMAGES] self.max_age = self.context.config.MAX_AGE self._calculate_dimensions() yield self._fetch_images() self.context.request.max_age = self.max_age
def create_preview(self, url, resolution=200): out_io = BytesIO() try: http_client = httpclient.AsyncHTTPClient() response = yield http_client.fetch(url) if not response.error: try: with (Image(blob=response.body, resolution=resolution)) as source: single_image = source.sequence[ 0] # Just work on first page with Image(single_image) as i: i.format = 'png' i.background_color = Color( 'white') # Set white background. i.alpha_channel = 'remove' # Remove transparency and replace with bg. i.save(file=out_io) raise gen.Return(out_io.getvalue()) except WandException as e: logger.exception('[PDFHander.create_preview] %s', e) raise gen.Return(None) else: logger.error('STATUS: %s - Failed to get pdf from url %s' % (str(400), url)) raise tornado.web.HTTPError(400) except httpclient.HTTPError as e: logger.error('STATUS: %s - Failed to get pdf from url %s' % (str(e.code), url)) valid_status_code = httputil.responses.get(e.code) if valid_status_code: self._error(e.code) else: raise tornado.web.HTTPError(400) finally: out_io.close()
def get_video_frame(context, url, normalize_url_func): """ A context manager that extracts a single frame out of a video file and stores it in a temporary file. Returns the path of the temporary file or None in case of failure. Depends on FFMPEG_PATH from Thumbor's configuration. """ url = normalize_url_func(url) # Fail nicely when ffmpeg cannot be found if not os.path.exists(context.config.FFMPEG_PATH): logger.error('%s does not exist, please configure FFMPEG_PATH', context.config.FFMPEG_PATH) yield None return # Prepare temporary file f, image_path = tempfile.mkstemp('.jpg') os.close(f) # Extract image try: cmd = [ context.config.FFMPEG_PATH, '-i', url, '-ss', '00:00:01.000', '-vframes', '1', '-y', '-nostats', '-loglevel', 'error', image_path ] subprocess.check_call(cmd) yield image_path except: logger.exception('Cannot extract image frame from %s', url) yield None finally: # Cleanup try_to_delete(image_path)
def __init__(self, context, importer): ''' :param context: :param importer: ''' ThumborContextImporter.__init__(self, context, importer) # Dynamically load registered modules for name in self._community_modules: if hasattr(importer, name): init = getattr(importer, name) if not hasattr(init, '__call__'): logger.error( "Attr {attr} of object {obj} is not callable".format( attr=name, obj=importer, )) instance = getattr(importer, name)(context) setattr(self, name, instance) else: logger.warning("Module {name} is not configured.".format( name=name.upper())) setattr(self, name, None)
def on_image_fetch(self): if (self.is_any_failed()): logger.error('filters.distributed_collage: Some images failed') self.callback() elif self.is_all_fetched(): self.assembly()
def get_image(self): """ This function is called after the PRE_LOAD filters have been applied. It applies the AFTER_LOAD filters on the result, then crops the image. """ try: result = yield self._fetch(self.context.request.image_url) if not result.successful: if result.loader_error == LoaderResult.ERROR_NOT_FOUND: self._error(404) return elif result.loader_error == LoaderResult.ERROR_UPSTREAM: # Return a Bad Gateway status if the error came from upstream self._error(502) return elif result.loader_error == LoaderResult.ERROR_TIMEOUT: # Return a Gateway Timeout status if upstream timed out (i.e. 599) self._error(504) return else: self._error(500) return except Exception as e: msg = '[BaseHandler] get_image failed for url `{url}`. error: `{error}`'.format( url=self.context.request.image_url, error=e) self.log_exception(*sys.exc_info()) if 'cannot identify image file' in e.message: logger.warning(msg) self._error(400) else: logger.error(msg) self._error(500) return normalized = result.normalized buffer = result.buffer engine = result.engine req = self.context.request if engine is None: if buffer is None: self._error(504) return engine = self.context.request.engine try: engine.load(buffer, self.context.request.extension) except Exception: self._error(504) return def transform(): self.normalize_crops(normalized, req, engine) if req.meta: self.context.request.engine = JSONEngine( engine, req.image_url, req.meta_callback) after_transform_cb = functools.partial(self.after_transform, self.context) Transformer(self.context).transform(after_transform_cb) self.filters_runner.apply_filters(thumbor.filters.PHASE_AFTER_LOAD, transform)
async def _fetch_images(self): crypto = CryptoURL(key=self.context.server.security_key) image_ops = [] if not hasattr(self.context.config, "DISTRIBUTED_COLLAGE_FILTER_HTTP_LOADER"): self.context.config.DISTRIBUTED_COLLAGE_FILTER_HTTP_LOADER = ( "thumbor.loaders.http_loader" ) self.context.modules.importer.import_item( "DISTRIBUTED_COLLAGE_FILTER_HTTP_LOADER" ) loader = self.context.modules.importer.distributed_collage_filter_http_loader for i, url in enumerate(self.urls): width = ( self.image_width if i < len(self.urls) - 1 else self.last_image_width ) height = ( self.context.request.height or self.context.transformer.get_target_dimensions()[1] ) params = { "width": int(width), "height": int(height), "image_url": url, "smart": True, "halign": "center", "valign": "middle", "filters": ["quality(100)"], } thumbor_host = getattr( self.context.config, "DISTRIBUTED_COLLAGE_FILTER_THUMBOR_SERVER_URL", "%s://%s" % ( self.context.request_handler.request.protocol, self.context.request_handler.request.host, ), ) encrypted_url = "%s%s" % (thumbor_host, crypto.generate(**params)) image_ops.append(await loader.load(self.context, encrypted_url)) successful = all([image.successful for image in image_ops]) if not successful: logger.error( "Retrieving at least one of the collaged images failed: %s" % ( ", ".join( [image.error for image in image_ops if not image.successful] ), ) ) return max_age = min( [ self.get_max_age(image.metadata.get("Cache-Control"), self.max_age) for image in image_ops ] ) self.assembly_images(image_ops)
def _error(self, status, msg=None): self.set_status(status) if msg is not None: logger.error(msg) self.finish()
def run_and_check_ssim_and_size( self, url, mediawiki_reference_thumbnail, perfect_reference_thumbnail, expected_width, expected_height, expected_ssim, size_tolerance, ): """Request URL and check ssim and size. Arguments: url -- thumbnail URL mediawiki_reference_thumbnail -- reference thumbnail file expected_width -- expected thumbnail width expected_height -- expected thumbnail height expected_ssim -- minimum SSIM score size_tolerance -- maximum file size ratio between reference and result perfect_reference_thumbnail -- perfect lossless version of the target thumbnail, for visual comparison """ try: result = self.fetch(url) except Exception as e: assert False, 'Exception occured: %r' % e assert result is not None, 'No result' assert result.code == 200, 'Response code: %s' % result.code result.buffer.seek(0) generated = Image.open(result.buffer) expected_path = os.path.join( os.path.dirname(__file__), 'thumbnails', mediawiki_reference_thumbnail ) visual_expected_path = os.path.join( os.path.dirname(__file__), 'thumbnails', perfect_reference_thumbnail ) visual_expected = Image.open(visual_expected_path).convert(generated.mode) assert generated.size[0] == expected_width, \ 'Width differs: %d (should be == %d)\n' % (generated.size[0], expected_width) assert generated.size[1] == expected_height, \ 'Height differs: %d (should be == %d)\n' % (generated.size[1], expected_height) ssim = compute_ssim(generated, visual_expected) try: assert ssim >= expected_ssim, 'Images too dissimilar: %f (should be >= %f)\n' % (ssim, expected_ssim) except AssertionError as e: output_file = NamedTemporaryFile(delete=False) output_file.write(result.buffer.getvalue()) output_file.close() logger.error('Dumped generated test image for debugging purposes: %s' % output_file.name) raise e expected_filesize = float(os.path.getsize(expected_path)) generated_filesize = float(len(result.buffer.getvalue())) ratio = generated_filesize / expected_filesize assert ratio <= size_tolerance, \ 'Generated file bigger than size tolerance: %f (should be <= %f)' % (ratio, size_tolerance) return result.buffer
async def get_image(self): """ This function is called after the PRE_LOAD filters have been applied. It applies the AFTER_LOAD filters on the result, then crops the image. """ try: result = await self._fetch(self.context.request.image_url) if not result.successful: if result.loader_error == LoaderResult.ERROR_NOT_FOUND: self._error(404) return if result.loader_error == LoaderResult.ERROR_UPSTREAM: # Return a Bad Gateway status if the error # came from upstream self._error(502) return if result.loader_error == LoaderResult.ERROR_TIMEOUT: # Return a Gateway Timeout status if upstream # timed out (i.e. 599) self._error(504) return if isinstance(result.loader_error, int): self._error(result.loader_error) return if (hasattr(result, "engine_error") and result.engine_error == EngineResult.COULD_NOT_LOAD_IMAGE): self._error(400) return self._error(500) return except Exception as error: msg = ( "[BaseHandler] get_image failed for url `{url}`. error: `{error}`" ).format(url=self.context.request.image_url, error=error) self.log_exception(*sys.exc_info()) if "cannot identify image file" in str(error): logger.warning(msg) self._error(400) else: logger.error(msg) self._error(500) return normalized = result.normalized buffer = result.buffer engine = result.engine req = self.context.request if engine is None: if buffer is None: self._error(504) return engine = self.context.request.engine try: engine.load(buffer, self.context.request.extension) except Exception as error: logger.exception("Loading image failed with %s", error) self._error(504) return self.context.transformer = Transformer(self.context) await self.filters_runner.apply_filters( thumbor.filters.PHASE_AFTER_LOAD) self.normalize_crops(normalized, req, engine) if req.meta: self.context.transformer.engine = self.context.request.engine = JSONEngine( engine, req.image_url, req.meta_callback) await self.context.transformer.transform() await self.after_transform()
def get_image(self): """ This function is called after the PRE_LOAD filters have been applied. It applies the AFTER_LOAD filters on the result, then crops the image. """ if self.context.request.mgnlogin: mgnl_auth = "" if self.context.config.MGNLOGIN_USER and self.context.config.MGNLOGIN_PASS: mgnl_auth = "?mgnlUserId=%s&mgnlUserPSWD=%s" % ( self.context.config.MGNLOGIN_USER, self.context.config.MGNLOGIN_PASS) req_image_url = self.context.request.image_url + mgnl_auth else: req_image_url = self.context.request.image_url try: result = yield self._fetch(req_image_url) if not result.successful: if result.loader_error == LoaderResult.ERROR_NOT_FOUND: self._error(404) return elif result.loader_error == LoaderResult.ERROR_UPSTREAM: # Return a Bad Gateway status if the error came from upstream self._error(502) return elif result.loader_error == LoaderResult.ERROR_TIMEOUT: # Return a Gateway Timeout status if upstream timed out (i.e. 599) self._error(504) return elif isinstance(result.loader_error, int): self._error(result.loader_error) return elif hasattr( result, 'engine_error' ) and result.engine_error == EngineResult.COULD_NOT_LOAD_IMAGE: self._error(400) return else: self._error(500) return except Exception as e: msg = '[BaseHandler] get_image failed for url `{url}`. error: `{error}`'.format( url=self.context.request.image_url, error=e) self.log_exception(*sys.exc_info()) if 'cannot identify image file' in e.message: logger.warning(msg) self._error(400) else: logger.error(msg) self._error(500) return normalized = result.normalized buffer = result.buffer engine = result.engine req = self.context.request if engine is None: if buffer is None: self._error(504) return engine = self.context.request.engine try: engine.load(buffer, self.context.request.extension) except Exception: self._error(504) return self.context.transformer = Transformer(self.context) def transform(): self.normalize_crops(normalized, req, engine) if req.meta: self.context.transformer.engine = \ self.context.request.engine = \ JSONEngine(engine, req.image_url, req.meta_callback) self.context.transformer.transform(self.after_transform) self.filters_runner.apply_filters(thumbor.filters.PHASE_AFTER_LOAD, transform)
async def red_eye(self) -> None: if not OPENCV_AVAILABLE: logger.error( "Can't use red eye removal filter if OpenCV and NumPy are not available." ) return faces = [ face for face in self.context.request.focal_points if face.origin == "Face Detection" ] if not faces: return mode, data = self.engine.image_data_as_rgb() mode = mode.lower() size = self.engine.size image = np.ndarray( shape=(size[1], size[0], 4 if mode == "rgba" else 3), dtype="|u1", buffer=data, ).copy() for face in faces: face_x = int(face.x - face.width / 2) face_y = int(face.y - face.height / 2) face_image = image[face_y:face_y + face.height, face_x:face_x + face.width] eye_rects = self.cascade.detectMultiScale( face_image, scaleFactor=HAAR_SCALE, minNeighbors=MIN_NEIGHBORS, minSize=MIN_SIZE, ) for pos_x, pos_y, width, height in eye_rects: # Crop the eye region eye_image = face_image[pos_y:pos_y + height, pos_x:pos_x + width] # split the images into 3 channels red, green, blue = cv2.split(eye_image) # Add blue and green channels blue_green = cv2.add(blue, green) mean = blue_green // 2 # threshold the mask based on red color and combination of blue and green color mask = ((red > RED_THRESHOLD * mean) & (red > 60)).astype(np.uint8) * 255 # Some extra region may also get detected , we find the largest region # find all contours contours_return = cv2.findContours( mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) # It return contours and Hierarchy if len(contours_return) == 2: contours, _ = contours_return else: _, contours, _ = contours_return # find contour with max area max_area = 0 max_cont = None for cont in contours: area = cv2.contourArea(cont) if area > max_area: max_area = area max_cont = cont if max_cont is None: continue mask = mask * 0 # Reset the mask image to complete black image # draw the biggest contour on mask cv2.drawContours(mask, [max_cont], 0, (255), -1) # Close the holes to make a smooth region mask = cv2.morphologyEx( mask, cv2.MORPH_CLOSE, cv2.getStructuringElement(cv2.MORPH_DILATE, (5, 5)), ) mask = cv2.dilate(mask, (3, 3), iterations=3) # The information of only red color is lost, # So we fill the mean of blue and green color in all # three channels(BGR) to maintain the texture # Fill this black mean value to masked image mean = cv2.bitwise_and(mean, mask) # mask the mean image mean = cv2.cvtColor( mean, cv2.COLOR_GRAY2RGB) # convert mean to 3 channel mask = cv2.cvtColor( mask, cv2.COLOR_GRAY2RGB) # convert mask to 3 channel eye = (cv2.bitwise_and(~mask, eye_image) + mean ) # Copy the mean color to masked region to color image face_image[pos_y:pos_y + height, pos_x:pos_x + width] = eye self.engine.set_image_data(image.tobytes())
def get_image(self): """ This function is called after the PRE_LOAD filters have been applied. It applies the AFTER_LOAD filters on the result, then crops the image. """ try: result = yield self._fetch( self.context.request.image_url ) if not result.successful: if result.loader_error == LoaderResult.ERROR_NOT_FOUND: self._error(404) return elif result.loader_error == LoaderResult.ERROR_FORBIDDEN: blacklist = yield self.get_blacklist_contents() blacklist += self.context.request.image_url + "\n" logger.warning('403 Adding to blacklist: %s' % self.context.request.image_url) self.context.modules.storage.put('blacklist.txt', blacklist) self._error(404) return elif result.loader_error == LoaderResult.ERROR_UPSTREAM: # Return a Bad Gateway status if the error came from upstream self._error(502) return elif result.loader_error == LoaderResult.ERROR_TIMEOUT: # Return a Gateway Timeout status if upstream timed out (i.e. 599) self._error(504) return elif isinstance(result.loader_error, int): self._error(result.loader_error) return elif hasattr(result, 'engine_error') and result.engine_error == EngineResult.COULD_NOT_LOAD_IMAGE: self._error(400) return else: self._error(500) return except Exception as e: url = self.context.request.image_url if self.context.request else '-|-' msg = '[BaseHandler] get_image failed for url `{url}`. error: `{error}`'.format( url=url, error=e ) self.log_exception(*sys.exc_info()) if 'cannot identify image file' in e.message: logger.warning(msg) self._error(400) else: logger.error(msg) self._error(500) return normalized = result.normalized buffer = result.buffer engine = result.engine req = self.context.request if engine is None: if buffer is None: self._error(504) return engine = self.context.request.engine try: engine.load(buffer, self.context.request.extension) except Exception: self._error(504) return self.context.transformer = Transformer(self.context) def transform(): self.normalize_crops(normalized, req, engine) if req.meta: self.context.transformer.engine = \ self.context.request.engine = \ JSONEngine(engine, req.image_url, req.meta_callback) self.context.transformer.transform(self.after_transform) self.filters_runner.apply_filters(thumbor.filters.PHASE_AFTER_LOAD, transform)