"""Read/Write PostScript files
"""
# Copyright (c) 2013-2019 Erling Andersen, Haukeland University Hospital,
# Bergen, Norway
import os.path
import locale
import logging
import magic
import tempfile
import numpy as np
from ghostscript import _gsprint as gs
from ghostscript import GhostscriptError
import imageio
import imagedata.formats
from imagedata.formats.abstractplugin import AbstractPlugin
logger = logging.getLogger(__name__)
[docs]class ImageTypeError(Exception):
"""
Thrown when trying to load or save an image of unknown type.
"""
pass
[docs]class DependencyError(Exception):
"""
Thrown when a required module could not be loaded.
"""
pass
[docs]class PSPlugin(AbstractPlugin):
"""Read PostScript files.
Writing PostScript files is not implemented."""
name = "ps"
description = "Read PostScript files."
authors = "Erling Andersen"
version = "1.0.0"
url = "www.helse-bergen.no"
def __init__(self):
super(PSPlugin, self).__init__(self.name, self.description,
self.authors, self.version, self.url)
def _read_image(self, f, opts, hdr):
"""Read image data from given file handle
Args:
self: format plugin instance
f: file handle or filename (depending on self._need_local_file)
opts: Input options (dict)
hdr: Header dict
Returns:
Tuple of
hdr: Header dict
Return values:
- info: Internal data for the plugin
None if the given file should not be included (e.g. raw file)
si: numpy array (multi-dimensional)
"""
self.dpi = 150 # dpi
self.driver = 'png16m'
self.rotate = 0
legal_attributes = {'dpi', 'driver', 'rotate'}
if 'psopt' in opts and opts['psopt']:
for expr in opts['psopt'].split(','):
attr,value = expr.split('=')
if attr in legal_attributes:
setattr(self, attr, value)
else:
raise ValueError('Unknown attribute {} set in psopt'.format(attr))
self.dpi = int(self.dpi)
self.rotate = int(self.rotate)
if self.rotate not in {0, 90}:
raise ValueError('psopt rotate value {} is not implemented'.format(self.rotate))
with tempfile.TemporaryDirectory() as tempdir:
logger.debug("PSPlugin.read: tempdir {}".format(tempdir))
try:
# Convert filename to PNG
self._convert_to_png(f, tempdir, "fname%02d.png")
# self._pdf_to_png(f, os.path.join(tempdir.name, "fname.png"))
except imagedata.formats.NotImageError:
raise imagedata.formats.NotImageError('{} does not look like a PostScript file'.format(f))
# Read the PNG file(s)
image_list = list()
for fname in sorted(os.listdir(tempdir)):
filename = os.path.join(tempdir, fname)
logger.debug("PSPlugin.read: call ITKPlugin {}".format(filename))
img = imageio.imread(filename)
image_list.append(img)
if len(image_list) < 1:
raise ValueError('No image data read')
img = image_list[0]
shape = (len(image_list),) + img.shape
dtype = img.dtype
si = np.zeros(shape, dtype)
for i, img in enumerate(image_list):
logger.debug('read: img {} si {}'.format(img.shape, si.shape))
si[i] = img
hdr['spacing'] = np.array([1,1,1])
# Color space: RGB
hdr['photometricInterpretation'] = 'MONOCHROME2'
hdr['color'] = False
if self.driver == 'png16m' and si.shape[-1] == 3:
# Photometric interpretation = 'RGB'
hdr['photometricInterpretation'] = 'RGB'
hdr['color'] = True
if self.rotate == 90:
si = np.rot90(si, axes=(1,2))
# Let a single page be a 2D image
if si.ndim == 3 and si.shape[0] == 1:
si.shape = si.shape[1:]
logger.debug('read: si {}'.format(si.shape))
return True, si
def _need_local_file(self):
"""Do the plugin need access to local files?
Returns:
Boolean
- True: The plugin need access to local filenames
- False: The plugin can access files given by an open file handle
"""
return True
def _set_tags(self, image_list, hdr, si):
"""Set header tags.
Args:
self: format plugin instance
image_list: list with (info,img) tuples
hdr: Header dict
si: numpy array (multi-dimensional)
Returns:
hdr: Header dict
"""
#super(PSPlugin, self)._set_tags(image_list, hdr, si)
# Default spacing and orientation
hdr['spacing'] = np.array([1,1,1])
hdr['imagePositions'] = {}
hdr['imagePositions'][0] = np.array([0,0,0])
hdr['orientation'] = np.array([0,1,0,-1,0,0])
# Set tags
axes = list()
_actual_shape = si.shape
_color = False
if 'color' in hdr and hdr['color']:
_actual_shape = si.shape[:-1]
_color = True
_actual_ndim = len(_actual_shape)
nz = 1
axes.append(imagedata.axis.UniformLengthAxis(
'row',
hdr['imagePositions'][0][1],
_actual_shape[-2],
hdr['spacing'][1])
)
axes.append(imagedata.axis.UniformLengthAxis(
'column',
hdr['imagePositions'][0][2],
_actual_shape[-1],
hdr['spacing'][2])
)
if _actual_ndim > 2:
nz = _actual_shape[-3]
axes.insert(0, imagedata.axis.UniformLengthAxis(
'slice',
hdr['imagePositions'][0][0],
nz,
hdr['spacing'][0])
)
if _color:
axes.append(imagedata.axis.VariableAxis(
'rgb',
['r', 'g', 'b'])
)
hdr['axes'] = axes
tags = {}
for slice in range(nz):
tags[slice] = np.array([0])
hdr['tags'] = tags
return
def _convert_to_png(self, filename, tempdir, fname):
"""Convert file from PostScript to PNG
Args:
filename: PostScript file
tempdir: Output directory
fname: Output filename
Multi-page PostScript files will be converted to fname-N.png
"""
# Verify that the input file is a PostScript file
if magic.from_file(filename, mime=True) != 'application/postscript' and \
magic.from_file(filename, mime=True) != 'application/pdf':
raise imagedata.formats.NotImageError('{} does not look like a PostScript or PDF file'.format(filename))
args = [
"gs", # actual value doesn't matter
"-dNOPAUSE", "-dBATCH", "-dSAFER", "-dQUIET",
# "-sDEVICE=pnggray",
# "-sDEVICE=png16m",
"-r{}".format(self.dpi),
"-sDEVICE={}".format(self.driver),
"-sOutputFile=" + os.path.join(tempdir, fname),
# "-c", ".setpdfwrite",
"-f", filename
]
# arguments have to be bytes, encode them
encoding = locale.getpreferredencoding()
args = [a.encode(encoding) for a in args]
logger.debug('_convert_to_png: args {}'.format(args))
try:
instance = gs.new_instance()
code = gs.init_with_args(instance, args)
code1 = gs.exit(instance)
if code == 0 or code == gs.e_Quit:
code = code1
gs.delete_instance(instance)
if not (code == 0 or code == gs.e_Quit):
raise DependencyError("Cannot run Ghostscript: {}".format(code))
except GhostscriptError as e:
logger.error('_convert_to_png: error: {}'.format(e))
raise DependencyError("Cannot run Ghostscript: {}".format(e))
# @staticmethod
# def _pdf_to_png(inputPath, outputPath):
# """Convert from pdf to png by using python gfx
#
# The resolution of the output png can be adjusted in the config file
# under General -> zoom, typical value 150
# The quality of the output png can be adjusted in the config file under
# General -> antiAlise, typical value 5
#
# :param inputPath: path to a pdf file
# :param outputPath: path to the location where the output png will be
# saved
# """
# print("converting pdf {} {}".format(inputPath, outputPath))
# gfx.setparameter("zoom", config.readConfig("zoom")) # Gives the image higher resolution
# doc = gfx.open("pdf", inputPath)
# img = gfx.ImageList()
#
# img.setparameter("antialise", config.readConfig("antiAliasing")) # turn on antialising
# page1 = doc.getPage(1)
# img.startpage(page1.width, page1.height)
# page1.render(img)
# img.endpage()
# img.save(outputPath)
[docs] def write_3d_numpy(self, si, destination, opts):
"""Write 3D numpy image as PostScript file
Args:
self: ITKPlugin instance
si: Series array (3D or 4D), including these attributes:
- slices,
- spacing,
- imagePositions,
- transformationMatrix,
- orientation,
- tags
destination: dict of archive and filenames
opts: Output options (dict)
Raises:
imagedata.formats.WriteNotImplemented: Always, writing is not implemented.
"""
raise imagedata.formats.WriteNotImplemented(
'Writing PostScript files is not implemented.')
[docs] def write_4d_numpy(self, si, destination, opts):
"""Write 4D numpy image as PostScript files
Args:
self: ITKPlugin instance
si[tag,slice,rows,columns]: Series array, including these attributes:
- slices,
- spacing,
- imagePositions,
- transformationMatrix,
- orientation,
- tags
destination: dict of archive and filenames
opts: Output options (dict)
Raises:
imagedata.formats.WriteNotImplemented: Always, writing is not implemented.
"""
raise imagedata.formats.WriteNotImplemented(
'Writing PostScript files is not implemented.')