diff options
Diffstat (limited to 'silx/gui/plot/backends/glutils')
-rw-r--r-- | silx/gui/plot/backends/glutils/GLPlotCurve.py | 1151 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLPlotFrame.py | 1116 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLPlotImage.py | 674 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLSupport.py | 201 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLText.py | 270 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLTexture.py | 239 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/PlotImageFile.py | 153 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/__init__.py | 44 |
8 files changed, 0 insertions, 3848 deletions
diff --git a/silx/gui/plot/backends/glutils/GLPlotCurve.py b/silx/gui/plot/backends/glutils/GLPlotCurve.py deleted file mode 100644 index 12b6bbe..0000000 --- a/silx/gui/plot/backends/glutils/GLPlotCurve.py +++ /dev/null @@ -1,1151 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -""" -This module provides classes to render 2D lines and scatter plots -""" - -from __future__ import division - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -import math -import logging -import warnings - -import numpy - -from silx.math.combo import min_max - -from ...._glutils import gl -from ...._glutils import Program, vertexBuffer -from .GLSupport import buildFillMaskIndices, mat4Identity, mat4Translate - - -_logger = logging.getLogger(__name__) - - -_MPL_NONES = None, 'None', '', ' ' -"""Possible values for None""" - - -def _notNaNSlices(array, length=1): - """Returns slices of none NaN values in the array. - - :param numpy.ndarray array: 1D array from which to get slices - :param int length: Slices shorter than length gets discarded - :return: Array of (start, end) slice indices - :rtype: numpy.ndarray - """ - isnan = numpy.isnan(numpy.array(array, copy=False).reshape(-1)) - notnan = numpy.logical_not(isnan) - start = numpy.where(numpy.logical_and(isnan[:-1], notnan[1:]))[0] + 1 - if notnan[0]: - start = numpy.append(0, start) - end = numpy.where(numpy.logical_and(notnan[:-1], isnan[1:]))[0] + 1 - if notnan[-1]: - end = numpy.append(end, len(array)) - slices = numpy.transpose((start, end)) - if length > 1: - # discard slices with less than length values - slices = slices[numpy.diff(slices, axis=1).ravel() >= length] - return slices - - -# fill ######################################################################## - -class _Fill2D(object): - """Object rendering curve filling as polygons - - :param numpy.ndarray xData: X coordinates of points - :param numpy.ndarray yData: Y coordinates of points - :param float baseline: Y value of the 'bottom' of the fill. - 0 for linear Y scale, -38 for log Y scale - :param List[float] color: RGBA color as 4 float in [0, 1] - :param List[float] offset: Translation of coordinates (ox, oy) - """ - - _PROGRAM = Program( - vertexShader=""" - #version 120 - - uniform mat4 matrix; - attribute float xPos; - attribute float yPos; - - void main(void) { - gl_Position = matrix * vec4(xPos, yPos, 0.0, 1.0); - } - """, - fragmentShader=""" - #version 120 - - uniform vec4 color; - - void main(void) { - gl_FragColor = color; - } - """, - attrib0='xPos') - - def __init__(self, xData=None, yData=None, - baseline=0, - color=(0., 0., 0., 1.), - offset=(0., 0.)): - self.xData = xData - self.yData = yData - self._xFillVboData = None - self._yFillVboData = None - self.color = color - self.offset = offset - - # Offset baseline - self.baseline = baseline - self.offset[1] - - def prepare(self): - """Rendering preparation: build indices and bounding box vertices""" - if (self._xFillVboData is None and - self.xData is not None and self.yData is not None): - - # Get slices of not NaN values longer than 1 element - isnan = numpy.logical_or(numpy.isnan(self.xData), - numpy.isnan(self.yData)) - notnan = numpy.logical_not(isnan) - start = numpy.where(numpy.logical_and(isnan[:-1], notnan[1:]))[0] + 1 - if notnan[0]: - start = numpy.append(0, start) - end = numpy.where(numpy.logical_and(notnan[:-1], isnan[1:]))[0] + 1 - if notnan[-1]: - end = numpy.append(end, len(isnan)) - slices = numpy.transpose((start, end)) - # discard slices with less than length values - slices = slices[numpy.diff(slices, axis=1).reshape(-1) >= 2] - - # Number of points: slice + 2 * leading and trailing points - # Twice leading and trailing points to produce degenerated triangles - nbPoints = numpy.sum(numpy.diff(slices, axis=1)) + 4 * len(slices) - points = numpy.empty((nbPoints, 2), dtype=numpy.float32) - - offset = 0 - for start, end in slices: - # Duplicate first point for connecting degenerated triangle - points[offset:offset+2] = self.xData[start], self.baseline - - # 2nd point of the polygon is last point - points[offset+2] = self.xData[end-1], self.baseline - - # Add all points from the data - indices = start + buildFillMaskIndices(end - start) - - points[offset+3:offset+3+len(indices), 0] = self.xData[indices] - points[offset+3:offset+3+len(indices), 1] = self.yData[indices] - - # Duplicate last point for connecting degenerated triangle - points[offset+3+len(indices)] = points[offset+3+len(indices)-1] - - offset += len(indices) + 4 - - self._xFillVboData, self._yFillVboData = vertexBuffer(points.T) - - def render(self, matrix): - """Perform rendering - - :param numpy.ndarray matrix: 4x4 transform matrix to use - """ - self.prepare() - - if self._xFillVboData is None: - return # Nothing to display - - self._PROGRAM.use() - - gl.glUniformMatrix4fv( - self._PROGRAM.uniforms['matrix'], 1, gl.GL_TRUE, - numpy.dot(matrix, - mat4Translate(*self.offset)).astype(numpy.float32)) - - gl.glUniform4f(self._PROGRAM.uniforms['color'], *self.color) - - xPosAttrib = self._PROGRAM.attributes['xPos'] - yPosAttrib = self._PROGRAM.attributes['yPos'] - - gl.glEnableVertexAttribArray(xPosAttrib) - self._xFillVboData.setVertexAttrib(xPosAttrib) - - gl.glEnableVertexAttribArray(yPosAttrib) - self._yFillVboData.setVertexAttrib(yPosAttrib) - - # Prepare fill mask - gl.glEnable(gl.GL_STENCIL_TEST) - gl.glStencilMask(1) - gl.glStencilFunc(gl.GL_ALWAYS, 1, 1) - gl.glStencilOp(gl.GL_INVERT, gl.GL_INVERT, gl.GL_INVERT) - gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE) - gl.glDepthMask(gl.GL_FALSE) - - gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, self._xFillVboData.size) - - gl.glStencilFunc(gl.GL_EQUAL, 1, 1) - # Reset stencil while drawing - gl.glStencilOp(gl.GL_ZERO, gl.GL_ZERO, gl.GL_ZERO) - gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE) - gl.glDepthMask(gl.GL_TRUE) - - # Draw directly in NDC - gl.glUniformMatrix4fv(self._PROGRAM.uniforms['matrix'], 1, gl.GL_TRUE, - mat4Identity().astype(numpy.float32)) - - # NDC vertices - gl.glVertexAttribPointer( - xPosAttrib, 1, gl.GL_FLOAT, gl.GL_FALSE, 0, - numpy.array((-1., -1., 1., 1.), dtype=numpy.float32)) - gl.glVertexAttribPointer( - yPosAttrib, 1, gl.GL_FLOAT, gl.GL_FALSE, 0, - numpy.array((-1., 1., -1., 1.), dtype=numpy.float32)) - - gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4) - - gl.glDisable(gl.GL_STENCIL_TEST) - - def discard(self): - """Release VBOs""" - if self._xFillVboData is not None: - self._xFillVboData.vbo.discard() - - self._xFillVboData = None - self._yFillVboData = None - - -# line ######################################################################## - -SOLID, DASHED, DASHDOT, DOTTED = '-', '--', '-.', ':' - - -class _Lines2D(object): - """Object rendering curve as a polyline - - :param xVboData: X coordinates VBO - :param yVboData: Y coordinates VBO - :param colorVboData: VBO of colors - :param distVboData: VBO of distance along the polyline - :param str style: Line style in: '-', '--', '-.', ':' - :param List[float] color: RGBA color as 4 float in [0, 1] - :param float width: Line width - :param float dashPeriod: Period of dashes - :param drawMode: OpenGL drawing mode - :param List[float] offset: Translation of coordinates (ox, oy) - """ - - STYLES = SOLID, DASHED, DASHDOT, DOTTED - """Supported line styles""" - - _SOLID_PROGRAM = Program( - vertexShader=""" - #version 120 - - uniform mat4 matrix; - attribute float xPos; - attribute float yPos; - attribute vec4 color; - - varying vec4 vColor; - - void main(void) { - gl_Position = matrix * vec4(xPos, yPos, 0., 1.) ; - vColor = color; - } - """, - fragmentShader=""" - #version 120 - - varying vec4 vColor; - - void main(void) { - gl_FragColor = vColor; - } - """, - attrib0='xPos') - - # Limitation: Dash using an estimate of distance in screen coord - # to avoid computing distance when viewport is resized - # results in inequal dashes when viewport aspect ratio is far from 1 - _DASH_PROGRAM = Program( - vertexShader=""" - #version 120 - - uniform mat4 matrix; - uniform vec2 halfViewportSize; - attribute float xPos; - attribute float yPos; - attribute vec4 color; - attribute float distance; - - varying float vDist; - varying vec4 vColor; - - void main(void) { - gl_Position = matrix * vec4(xPos, yPos, 0., 1.); - //Estimate distance in pixels - vec2 probe = vec2(matrix * vec4(1., 1., 0., 0.)) * - halfViewportSize; - float pixelPerDataEstimate = length(probe)/sqrt(2.); - vDist = distance * pixelPerDataEstimate; - vColor = color; - } - """, - fragmentShader=""" - #version 120 - - /* Dashes: [0, x], [y, z] - Dash period: w */ - uniform vec4 dash; - - varying float vDist; - varying vec4 vColor; - - void main(void) { - float dist = mod(vDist, dash.w); - if ((dist > dash.x && dist < dash.y) || dist > dash.z) { - discard; - } - gl_FragColor = vColor; - } - """, - attrib0='xPos') - - def __init__(self, xVboData=None, yVboData=None, - colorVboData=None, distVboData=None, - style=SOLID, color=(0., 0., 0., 1.), - width=1, dashPeriod=20, drawMode=None, - offset=(0., 0.)): - self.xVboData = xVboData - self.yVboData = yVboData - self.distVboData = distVboData - self.colorVboData = colorVboData - self.useColorVboData = colorVboData is not None - - self.color = color - self.width = width - self._style = None - self.style = style - self.dashPeriod = dashPeriod - self.offset = offset - - self._drawMode = drawMode if drawMode is not None else gl.GL_LINE_STRIP - - @property - def style(self): - """Line style (Union[str,None])""" - return self._style - - @style.setter - def style(self, style): - if style in _MPL_NONES: - self._style = None - else: - assert style in self.STYLES - self._style = style - - @classmethod - def init(cls): - """OpenGL context initialization""" - gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST) - - def render(self, matrix): - """Perform rendering - - :param numpy.ndarray matrix: 4x4 transform matrix to use - """ - style = self.style - if style is None: - return - - elif style == SOLID: - program = self._SOLID_PROGRAM - program.use() - - else: # DASHED, DASHDOT, DOTTED - program = self._DASH_PROGRAM - program.use() - - x, y, viewWidth, viewHeight = gl.glGetFloatv(gl.GL_VIEWPORT) - gl.glUniform2f(program.uniforms['halfViewportSize'], - 0.5 * viewWidth, 0.5 * viewHeight) - - if self.style == DOTTED: - dash = (0.1 * self.dashPeriod, - 0.6 * self.dashPeriod, - 0.7 * self.dashPeriod, - self.dashPeriod) - elif self.style == DASHDOT: - dash = (0.3 * self.dashPeriod, - 0.5 * self.dashPeriod, - 0.6 * self.dashPeriod, - self.dashPeriod) - else: - dash = (0.5 * self.dashPeriod, - self.dashPeriod, - self.dashPeriod, - self.dashPeriod) - - gl.glUniform4f(program.uniforms['dash'], *dash) - - distAttrib = program.attributes['distance'] - gl.glEnableVertexAttribArray(distAttrib) - self.distVboData.setVertexAttrib(distAttrib) - - gl.glEnable(gl.GL_LINE_SMOOTH) - - matrix = numpy.dot(matrix, - mat4Translate(*self.offset)).astype(numpy.float32) - gl.glUniformMatrix4fv(program.uniforms['matrix'], - 1, gl.GL_TRUE, matrix) - - colorAttrib = program.attributes['color'] - if self.useColorVboData and self.colorVboData is not None: - gl.glEnableVertexAttribArray(colorAttrib) - self.colorVboData.setVertexAttrib(colorAttrib) - else: - gl.glDisableVertexAttribArray(colorAttrib) - gl.glVertexAttrib4f(colorAttrib, *self.color) - - xPosAttrib = program.attributes['xPos'] - gl.glEnableVertexAttribArray(xPosAttrib) - self.xVboData.setVertexAttrib(xPosAttrib) - - yPosAttrib = program.attributes['yPos'] - gl.glEnableVertexAttribArray(yPosAttrib) - self.yVboData.setVertexAttrib(yPosAttrib) - - gl.glLineWidth(self.width) - gl.glDrawArrays(self._drawMode, 0, self.xVboData.size) - - gl.glDisable(gl.GL_LINE_SMOOTH) - - -def _distancesFromArrays(xData, yData): - """Returns distances between each points - - :param numpy.ndarray xData: X coordinate of points - :param numpy.ndarray yData: Y coordinate of points - :rtype: numpy.ndarray - """ - deltas = numpy.dstack(( - numpy.ediff1d(xData, to_begin=numpy.float32(0.)), - numpy.ediff1d(yData, to_begin=numpy.float32(0.))))[0] - return numpy.cumsum(numpy.sqrt(numpy.sum(deltas ** 2, axis=1))) - - -# points ###################################################################### - -DIAMOND, CIRCLE, SQUARE, PLUS, X_MARKER, POINT, PIXEL, ASTERISK = \ - 'd', 'o', 's', '+', 'x', '.', ',', '*' - -H_LINE, V_LINE = '_', '|' - - -class _Points2D(object): - """Object rendering curve markers - - :param xVboData: X coordinates VBO - :param yVboData: Y coordinates VBO - :param colorVboData: VBO of colors - :param str marker: Kind of symbol to use, see :attr:`MARKERS`. - :param List[float] color: RGBA color as 4 float in [0, 1] - :param float size: Marker size - :param List[float] offset: Translation of coordinates (ox, oy) - """ - - MARKERS = (DIAMOND, CIRCLE, SQUARE, PLUS, X_MARKER, POINT, PIXEL, ASTERISK, - H_LINE, V_LINE) - """List of supported markers""" - - _VERTEX_SHADER = """ - #version 120 - - uniform mat4 matrix; - uniform int transform; - uniform float size; - attribute float xPos; - attribute float yPos; - attribute vec4 color; - - varying vec4 vColor; - - void main(void) { - gl_Position = matrix * vec4(xPos, yPos, 0., 1.); - vColor = color; - gl_PointSize = size; - } - """ - - _FRAGMENT_SHADER_SYMBOLS = { - DIAMOND: """ - float alphaSymbol(vec2 coord, float size) { - vec2 centerCoord = abs(coord - vec2(0.5, 0.5)); - float f = centerCoord.x + centerCoord.y; - return clamp(size * (0.5 - f), 0.0, 1.0); - } - """, - CIRCLE: """ - float alphaSymbol(vec2 coord, float size) { - float radius = 0.5; - float r = distance(coord, vec2(0.5, 0.5)); - return clamp(size * (radius - r), 0.0, 1.0); - } - """, - SQUARE: """ - float alphaSymbol(vec2 coord, float size) { - return 1.0; - } - """, - PLUS: """ - float alphaSymbol(vec2 coord, float size) { - vec2 d = abs(size * (coord - vec2(0.5, 0.5))); - if (min(d.x, d.y) < 0.5) { - return 1.0; - } else { - return 0.0; - } - } - """, - X_MARKER: """ - float alphaSymbol(vec2 coord, float size) { - vec2 pos = floor(size * coord) + 0.5; - vec2 d_x = abs(pos.x + vec2(- pos.y, pos.y - size)); - if (min(d_x.x, d_x.y) <= 0.5) { - return 1.0; - } else { - return 0.0; - } - } - """, - ASTERISK: """ - float alphaSymbol(vec2 coord, float size) { - /* Combining +, x and circle */ - vec2 d_plus = abs(size * (coord - vec2(0.5, 0.5))); - vec2 pos = floor(size * coord) + 0.5; - vec2 d_x = abs(pos.x + vec2(- pos.y, pos.y - size)); - if (min(d_plus.x, d_plus.y) < 0.5) { - return 1.0; - } else if (min(d_x.x, d_x.y) <= 0.5) { - float r = distance(coord, vec2(0.5, 0.5)); - return clamp(size * (0.5 - r), 0.0, 1.0); - } else { - return 0.0; - } - } - """, - H_LINE: """ - float alphaSymbol(vec2 coord, float size) { - float dy = abs(size * (coord.y - 0.5)); - if (dy < 0.5) { - return 1.0; - } else { - return 0.0; - } - } - """, - V_LINE: """ - float alphaSymbol(vec2 coord, float size) { - float dx = abs(size * (coord.x - 0.5)); - if (dx < 0.5) { - return 1.0; - } else { - return 0.0; - } - } - """ - } - - _FRAGMENT_SHADER_TEMPLATE = """ - #version 120 - - uniform float size; - - varying vec4 vColor; - - %s - - void main(void) { - float alpha = alphaSymbol(gl_PointCoord, size); - if (alpha <= 0.0) { - discard; - } else { - gl_FragColor = vec4(vColor.rgb, alpha * clamp(vColor.a, 0.0, 1.0)); - } - } - """ - - _PROGRAMS = {} - - def __init__(self, xVboData=None, yVboData=None, colorVboData=None, - marker=SQUARE, color=(0., 0., 0., 1.), size=7, - offset=(0., 0.)): - self.color = color - self._marker = None - self.marker = marker - self.size = size - self.offset = offset - - self.xVboData = xVboData - self.yVboData = yVboData - self.colorVboData = colorVboData - self.useColorVboData = colorVboData is not None - - @property - def marker(self): - """Symbol used to display markers (str)""" - return self._marker - - @marker.setter - def marker(self, marker): - if marker in _MPL_NONES: - self._marker = None - else: - assert marker in self.MARKERS - self._marker = marker - - @classmethod - def _getProgram(cls, marker): - """On-demand shader program creation.""" - if marker == PIXEL: - marker = SQUARE - elif marker == POINT: - marker = CIRCLE - - if marker not in cls._PROGRAMS: - cls._PROGRAMS[marker] = Program( - vertexShader=cls._VERTEX_SHADER, - fragmentShader=(cls._FRAGMENT_SHADER_TEMPLATE % - cls._FRAGMENT_SHADER_SYMBOLS[marker]), - attrib0='xPos') - - return cls._PROGRAMS[marker] - - @classmethod - def init(cls): - """OpenGL context initialization""" - version = gl.glGetString(gl.GL_VERSION) - majorVersion = int(version[0]) - assert majorVersion >= 2 - gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) # OpenGL 2 - gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2 - if majorVersion >= 3: # OpenGL 3 - gl.glEnable(gl.GL_PROGRAM_POINT_SIZE) - - def render(self, matrix): - """Perform rendering - - :param numpy.ndarray matrix: 4x4 transform matrix to use - """ - if self.marker is None: - return - - program = self._getProgram(self.marker) - program.use() - - matrix = numpy.dot(matrix, - mat4Translate(*self.offset)).astype(numpy.float32) - gl.glUniformMatrix4fv(program.uniforms['matrix'], 1, gl.GL_TRUE, matrix) - - if self.marker == PIXEL: - size = 1 - elif self.marker == POINT: - size = math.ceil(0.5 * self.size) + 1 # Mimic Matplotlib point - else: - size = self.size - gl.glUniform1f(program.uniforms['size'], size) - # gl.glPointSize(self.size) - - cAttrib = program.attributes['color'] - if self.useColorVboData and self.colorVboData is not None: - gl.glEnableVertexAttribArray(cAttrib) - self.colorVboData.setVertexAttrib(cAttrib) - else: - gl.glDisableVertexAttribArray(cAttrib) - gl.glVertexAttrib4f(cAttrib, *self.color) - - xAttrib = program.attributes['xPos'] - gl.glEnableVertexAttribArray(xAttrib) - self.xVboData.setVertexAttrib(xAttrib) - - yAttrib = program.attributes['yPos'] - gl.glEnableVertexAttribArray(yAttrib) - self.yVboData.setVertexAttrib(yAttrib) - - gl.glDrawArrays(gl.GL_POINTS, 0, self.xVboData.size) - - gl.glUseProgram(0) - - -# error bars ################################################################## - -class _ErrorBars(object): - """Display errors bars. - - This is using its own VBO as opposed to fill/points/lines. - There is no picking on error bars. - - It uses 2 vertices per error bars and uses :class:`_Lines2D` to - render error bars and :class:`_Points2D` to render the ends. - - :param numpy.ndarray xData: X coordinates of the data. - :param numpy.ndarray yData: Y coordinates of the data. - :param xError: The absolute error on the X axis. - :type xError: A float, or a numpy.ndarray of float32. - If it is an array, it can either be a 1D array of - same length as the data or a 2D array with 2 rows - of same length as the data: row 0 for negative errors, - row 1 for positive errors. - :param yError: The absolute error on the Y axis. - :type yError: A float, or a numpy.ndarray of float32. See xError. - :param float xMin: The min X value already computed by GLPlotCurve2D. - :param float yMin: The min Y value already computed by GLPlotCurve2D. - :param List[float] color: RGBA color as 4 float in [0, 1] - :param List[float] offset: Translation of coordinates (ox, oy) - """ - - def __init__(self, xData, yData, xError, yError, - xMin, yMin, - color=(0., 0., 0., 1.), - offset=(0., 0.)): - self._attribs = None - self._xMin, self._yMin = xMin, yMin - self.offset = offset - - if xError is not None or yError is not None: - self._xData = numpy.array( - xData, order='C', dtype=numpy.float32, copy=False) - self._yData = numpy.array( - yData, order='C', dtype=numpy.float32, copy=False) - - # This also works if xError, yError is a float/int - self._xError = numpy.array( - xError, order='C', dtype=numpy.float32, copy=False) - self._yError = numpy.array( - yError, order='C', dtype=numpy.float32, copy=False) - else: - self._xData, self._yData = None, None - self._xError, self._yError = None, None - - self._lines = _Lines2D( - None, None, color=color, drawMode=gl.GL_LINES, offset=offset) - self._xErrPoints = _Points2D( - None, None, color=color, marker=V_LINE, offset=offset) - self._yErrPoints = _Points2D( - None, None, color=color, marker=H_LINE, offset=offset) - - def _buildVertices(self): - """Generates error bars vertices""" - nbLinesPerDataPts = (0 if self._xError is None else 2) + \ - (0 if self._yError is None else 2) - - nbDataPts = len(self._xData) - - # interleave coord+error, coord-error. - # xError vertices first if any, then yError vertices if any. - xCoords = numpy.empty(nbDataPts * nbLinesPerDataPts * 2, - dtype=numpy.float32) - yCoords = numpy.empty(nbDataPts * nbLinesPerDataPts * 2, - dtype=numpy.float32) - - if self._xError is not None: # errors on the X axis - if len(self._xError.shape) == 2: - xErrorMinus, xErrorPlus = self._xError[0], self._xError[1] - else: - # numpy arrays of len 1 or len(xData) - xErrorMinus, xErrorPlus = self._xError, self._xError - - # Interleave vertices for xError - endXError = 4 * nbDataPts - xCoords[0:endXError-3:4] = self._xData + xErrorPlus - xCoords[1:endXError-2:4] = self._xData - xCoords[2:endXError-1:4] = self._xData - xCoords[3:endXError:4] = self._xData - xErrorMinus - - yCoords[0:endXError-3:4] = self._yData - yCoords[1:endXError-2:4] = self._yData - yCoords[2:endXError-1:4] = self._yData - yCoords[3:endXError:4] = self._yData - - else: - endXError = 0 - - if self._yError is not None: # errors on the Y axis - if len(self._yError.shape) == 2: - yErrorMinus, yErrorPlus = self._yError[0], self._yError[1] - else: - # numpy arrays of len 1 or len(yData) - yErrorMinus, yErrorPlus = self._yError, self._yError - - # Interleave vertices for yError - xCoords[endXError::4] = self._xData - xCoords[endXError+1::4] = self._xData - xCoords[endXError+2::4] = self._xData - xCoords[endXError+3::4] = self._xData - - yCoords[endXError::4] = self._yData + yErrorPlus - yCoords[endXError+1::4] = self._yData - yCoords[endXError+2::4] = self._yData - yCoords[endXError+3::4] = self._yData - yErrorMinus - - return xCoords, yCoords - - def prepare(self): - """Rendering preparation: build indices and bounding box vertices""" - if self._xData is None: - return - - if self._attribs is None: - xCoords, yCoords = self._buildVertices() - - xAttrib, yAttrib = vertexBuffer((xCoords, yCoords)) - self._attribs = xAttrib, yAttrib - - self._lines.xVboData = xAttrib - self._lines.yVboData = yAttrib - - # Set xError points using the same VBO as lines - self._xErrPoints.xVboData = xAttrib.copy() - self._xErrPoints.xVboData.size //= 2 - self._xErrPoints.yVboData = yAttrib.copy() - self._xErrPoints.yVboData.size //= 2 - - # Set yError points using the same VBO as lines - self._yErrPoints.xVboData = xAttrib.copy() - self._yErrPoints.xVboData.size //= 2 - self._yErrPoints.xVboData.offset += (xAttrib.itemsize * - xAttrib.size // 2) - self._yErrPoints.yVboData = yAttrib.copy() - self._yErrPoints.yVboData.size //= 2 - self._yErrPoints.yVboData.offset += (yAttrib.itemsize * - yAttrib.size // 2) - - def render(self, matrix): - """Perform rendering - - :param numpy.ndarray matrix: 4x4 transform matrix to use - """ - self.prepare() - - if self._attribs is not None: - self._lines.render(matrix) - self._xErrPoints.render(matrix) - self._yErrPoints.render(matrix) - - def discard(self): - """Release VBOs""" - if self._attribs is not None: - self._lines.xVboData, self._lines.yVboData = None, None - self._xErrPoints.xVboData, self._xErrPoints.yVboData = None, None - self._yErrPoints.xVboData, self._yErrPoints.yVboData = None, None - self._attribs[0].vbo.discard() - self._attribs = None - - -# curves ###################################################################### - -def _proxyProperty(*componentsAttributes): - """Create a property to access an attribute of attribute(s). - Useful for composition. - Supports multiple components this way: - getter returns the first found, setter sets all - """ - def getter(self): - for compName, attrName in componentsAttributes: - try: - component = getattr(self, compName) - except AttributeError: - pass - else: - return getattr(component, attrName) - - def setter(self, value): - for compName, attrName in componentsAttributes: - component = getattr(self, compName) - setattr(component, attrName, value) - return property(getter, setter) - - -class GLPlotCurve2D(object): - def __init__(self, xData, yData, colorData=None, - xError=None, yError=None, - lineStyle=SOLID, - lineColor=(0., 0., 0., 1.), - lineWidth=1, - lineDashPeriod=20, - marker=SQUARE, - markerColor=(0., 0., 0., 1.), - markerSize=7, - fillColor=None, - isYLog=False): - - self.colorData = colorData - - # Compute x bounds - if xError is None: - self.xMin, self.xMax = min_max(xData, min_positive=False) - else: - # Takes the error into account - if hasattr(xError, 'shape') and len(xError.shape) == 2: - xErrorMinus, xErrorPlus = xError[0], xError[1] - else: - xErrorMinus, xErrorPlus = xError, xError - self.xMin = numpy.nanmin(xData - xErrorMinus) - self.xMax = numpy.nanmax(xData + xErrorPlus) - - # Compute y bounds - if yError is None: - self.yMin, self.yMax = min_max(yData, min_positive=False) - else: - # Takes the error into account - if hasattr(yError, 'shape') and len(yError.shape) == 2: - yErrorMinus, yErrorPlus = yError[0], yError[1] - else: - yErrorMinus, yErrorPlus = yError, yError - self.yMin = numpy.nanmin(yData - yErrorMinus) - self.yMax = numpy.nanmax(yData + yErrorPlus) - - # Handle data offset - if xData.itemsize > 4 or yData.itemsize > 4: # Use normalization - # offset data, do not offset error as it is relative - self.offset = self.xMin, self.yMin - self.xData = (xData - self.offset[0]).astype(numpy.float32) - self.yData = (yData - self.offset[1]).astype(numpy.float32) - - else: # float32 - self.offset = 0., 0. - self.xData = xData - self.yData = yData - - if fillColor is not None: - # Use different baseline depending of Y log scale - self.fill = _Fill2D(self.xData, self.yData, - baseline=-38 if isYLog else 0, - color=fillColor, - offset=self.offset) - else: - self.fill = None - - self._errorBars = _ErrorBars(self.xData, self.yData, - xError, yError, - self.xMin, self.yMin, - offset=self.offset) - - self.lines = _Lines2D() - self.lines.style = lineStyle - self.lines.color = lineColor - self.lines.width = lineWidth - self.lines.dashPeriod = lineDashPeriod - self.lines.offset = self.offset - - self.points = _Points2D() - self.points.marker = marker - self.points.color = markerColor - self.points.size = markerSize - self.points.offset = self.offset - - xVboData = _proxyProperty(('lines', 'xVboData'), ('points', 'xVboData')) - - yVboData = _proxyProperty(('lines', 'yVboData'), ('points', 'yVboData')) - - colorVboData = _proxyProperty(('lines', 'colorVboData'), - ('points', 'colorVboData')) - - useColorVboData = _proxyProperty(('lines', 'useColorVboData'), - ('points', 'useColorVboData')) - - distVboData = _proxyProperty(('lines', 'distVboData')) - - lineStyle = _proxyProperty(('lines', 'style')) - - lineColor = _proxyProperty(('lines', 'color')) - - lineWidth = _proxyProperty(('lines', 'width')) - - lineDashPeriod = _proxyProperty(('lines', 'dashPeriod')) - - marker = _proxyProperty(('points', 'marker')) - - markerColor = _proxyProperty(('points', 'color')) - - markerSize = _proxyProperty(('points', 'size')) - - @classmethod - def init(cls): - """OpenGL context initialization""" - _Lines2D.init() - _Points2D.init() - - def prepare(self): - """Rendering preparation: build indices and bounding box vertices""" - if self.xVboData is None: - xAttrib, yAttrib, cAttrib, dAttrib = None, None, None, None - if self.lineStyle in (DASHED, DASHDOT, DOTTED): - dists = _distancesFromArrays(self.xData, self.yData) - if self.colorData is None: - xAttrib, yAttrib, dAttrib = vertexBuffer( - (self.xData, self.yData, dists)) - else: - xAttrib, yAttrib, cAttrib, dAttrib = vertexBuffer( - (self.xData, self.yData, self.colorData, dists)) - elif self.colorData is None: - xAttrib, yAttrib = vertexBuffer((self.xData, self.yData)) - else: - xAttrib, yAttrib, cAttrib = vertexBuffer( - (self.xData, self.yData, self.colorData)) - - self.xVboData = xAttrib - self.yVboData = yAttrib - self.distVboData = dAttrib - - if cAttrib is not None and self.colorData.dtype.kind == 'u': - cAttrib.normalization = True # Normalize uint to [0, 1] - self.colorVboData = cAttrib - self.useColorVboData = cAttrib is not None - - def render(self, matrix, isXLog, isYLog): - """Perform rendering - - :param numpy.ndarray matrix: 4x4 transform matrix to use - :param bool isXLog: - :param bool isYLog: - """ - self.prepare() - if self.fill is not None: - self.fill.render(matrix) - self._errorBars.render(matrix) - self.lines.render(matrix) - self.points.render(matrix) - - def discard(self): - """Release VBOs""" - if self.xVboData is not None: - self.xVboData.vbo.discard() - - self.xVboData = None - self.yVboData = None - self.colorVboData = None - self.distVboData = None - - self._errorBars.discard() - if self.fill is not None: - self.fill.discard() - - def pick(self, xPickMin, yPickMin, xPickMax, yPickMax): - """Perform picking on the curve according to its rendering. - - The picking area is [xPickMin, xPickMax], [yPickMin, yPickMax]. - - In case a segment between 2 points with indices i, i+1 is picked, - only its lower index end point (i.e., i) is added to the result. - In case an end point with index i is picked it is added to the result, - and the segment [i-1, i] is not tested for picking. - - :return: The indices of the picked data - :rtype: list of int - """ - if (self.marker is None and self.lineStyle is None) or \ - self.xMin > xPickMax or xPickMin > self.xMax or \ - self.yMin > yPickMax or yPickMin > self.yMax: - return None - - # offset picking bounds - xPickMin = xPickMin - self.offset[0] - xPickMax = xPickMax - self.offset[0] - yPickMin = yPickMin - self.offset[1] - yPickMax = yPickMax - self.offset[1] - - if self.lineStyle is not None: - # Using Cohen-Sutherland algorithm for line clipping - with warnings.catch_warnings(): # Ignore NaN comparison warnings - warnings.simplefilter('ignore', category=RuntimeWarning) - codes = ((self.yData > yPickMax) << 3) | \ - ((self.yData < yPickMin) << 2) | \ - ((self.xData > xPickMax) << 1) | \ - (self.xData < xPickMin) - - notNaN = numpy.logical_not(numpy.logical_or( - numpy.isnan(self.xData), numpy.isnan(self.yData))) - - # Add all points that are inside the picking area - indices = numpy.nonzero( - numpy.logical_and(codes == 0, notNaN))[0].tolist() - - # Segment that might cross the area with no end point inside it - segToTestIdx = numpy.nonzero((codes[:-1] != 0) & - (codes[1:] != 0) & - ((codes[:-1] & codes[1:]) == 0))[0] - - TOP, BOTTOM, RIGHT, LEFT = (1 << 3), (1 << 2), (1 << 1), (1 << 0) - - for index in segToTestIdx: - if index not in indices: - x0, y0 = self.xData[index], self.yData[index] - x1, y1 = self.xData[index + 1], self.yData[index + 1] - code1 = codes[index + 1] - - # check for crossing with horizontal bounds - # y0 == y1 is a never event: - # => pt0 and pt1 in same vertical area are not in segToTest - if code1 & TOP: - x = x0 + (x1 - x0) * (yPickMax - y0) / (y1 - y0) - elif code1 & BOTTOM: - x = x0 + (x1 - x0) * (yPickMin - y0) / (y1 - y0) - else: - x = None # No horizontal bounds intersection test - - if x is not None and xPickMin <= x <= xPickMax: - # Intersection - indices.append(index) - - else: - # check for crossing with vertical bounds - # x0 == x1 is a never event (see remark for y) - if code1 & RIGHT: - y = y0 + (y1 - y0) * (xPickMax - x0) / (x1 - x0) - elif code1 & LEFT: - y = y0 + (y1 - y0) * (xPickMin - x0) / (x1 - x0) - else: - y = None # No vertical bounds intersection test - - if y is not None and yPickMin <= y <= yPickMax: - # Intersection - indices.append(index) - - indices.sort() - - else: - with warnings.catch_warnings(): # Ignore NaN comparison warnings - warnings.simplefilter('ignore', category=RuntimeWarning) - indices = numpy.nonzero((self.xData >= xPickMin) & - (self.xData <= xPickMax) & - (self.yData >= yPickMin) & - (self.yData <= yPickMax))[0].tolist() - - return indices diff --git a/silx/gui/plot/backends/glutils/GLPlotFrame.py b/silx/gui/plot/backends/glutils/GLPlotFrame.py deleted file mode 100644 index 4ad1547..0000000 --- a/silx/gui/plot/backends/glutils/GLPlotFrame.py +++ /dev/null @@ -1,1116 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -""" -This modules provides the rendering of plot titles, axes and grid. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -# TODO -# keep aspect ratio managed here? -# smarter dirty flag handling? - -import datetime as dt -import math -import weakref -import logging -from collections import namedtuple - -import numpy - -from ...._glutils import gl, Program -from ..._utils import FLOAT32_SAFE_MIN, FLOAT32_MINPOS, FLOAT32_SAFE_MAX -from .GLSupport import mat4Ortho -from .GLText import Text2D, CENTER, BOTTOM, TOP, LEFT, RIGHT, ROTATE_270 -from ..._utils.ticklayout import niceNumbersAdaptative, niceNumbersForLog10 -from ..._utils.dtime_ticklayout import calcTicksAdaptive, bestFormatString -from ..._utils.dtime_ticklayout import timestamp - -_logger = logging.getLogger(__name__) - - -# PlotAxis #################################################################### - -class PlotAxis(object): - """Represents a 1D axis of the plot. - This class is intended to be used with :class:`GLPlotFrame`. - """ - - def __init__(self, plot, - tickLength=(0., 0.), - labelAlign=CENTER, labelVAlign=CENTER, - titleAlign=CENTER, titleVAlign=CENTER, - titleRotate=0, titleOffset=(0., 0.)): - self._ticks = None - - self._plot = weakref.ref(plot) - - self._isDateTime = False - self._timeZone = None - self._isLog = False - self._dataRange = 1., 100. - self._displayCoords = (0., 0.), (1., 0.) - self._title = '' - - self._tickLength = tickLength - self._labelAlign = labelAlign - self._labelVAlign = labelVAlign - self._titleAlign = titleAlign - self._titleVAlign = titleVAlign - self._titleRotate = titleRotate - self._titleOffset = titleOffset - - @property - def dataRange(self): - """The range of the data represented on the axis as a tuple - of 2 floats: (min, max).""" - return self._dataRange - - @dataRange.setter - def dataRange(self, dataRange): - assert len(dataRange) == 2 - assert dataRange[0] <= dataRange[1] - dataRange = float(dataRange[0]), float(dataRange[1]) - - if dataRange != self._dataRange: - self._dataRange = dataRange - self._dirtyTicks() - - @property - def isLog(self): - """Whether the axis is using a log10 scale or not as a bool.""" - return self._isLog - - @isLog.setter - def isLog(self, isLog): - isLog = bool(isLog) - if isLog != self._isLog: - self._isLog = isLog - self._dirtyTicks() - - @property - def timeZone(self): - """Returnss datetime.tzinfo that is used if this axis plots date times.""" - return self._timeZone - - @timeZone.setter - def timeZone(self, tz): - """Sets dateetime.tzinfo that is used if this axis plots date times.""" - self._timeZone = tz - self._dirtyTicks() - - @property - def isTimeSeries(self): - """Whether the axis is showing floats as datetime objects""" - return self._isDateTime - - @isTimeSeries.setter - def isTimeSeries(self, isTimeSeries): - isTimeSeries = bool(isTimeSeries) - if isTimeSeries != self._isDateTime: - self._isDateTime = isTimeSeries - self._dirtyTicks() - - @property - def displayCoords(self): - """The coordinates of the start and end points of the axis - in display space (i.e., in pixels) as a tuple of 2 tuples of - 2 floats: ((x0, y0), (x1, y1)). - """ - return self._displayCoords - - @displayCoords.setter - def displayCoords(self, displayCoords): - assert len(displayCoords) == 2 - assert len(displayCoords[0]) == 2 - assert len(displayCoords[1]) == 2 - displayCoords = tuple(displayCoords[0]), tuple(displayCoords[1]) - if displayCoords != self._displayCoords: - self._displayCoords = displayCoords - self._dirtyTicks() - - @property - def title(self): - """The text label associated with this axis as a str in latin-1.""" - return self._title - - @title.setter - def title(self, title): - if title != self._title: - self._title = title - - plot = self._plot() - if plot is not None: - plot._dirty() - - @property - def ticks(self): - """Ticks as tuples: ((x, y) in display, dataPos, textLabel).""" - if self._ticks is None: - self._ticks = tuple(self._ticksGenerator()) - return self._ticks - - def getVerticesAndLabels(self): - """Create the list of vertices for axis and associated text labels. - - :returns: A tuple: List of 2D line vertices, List of Text2D labels. - """ - vertices = list(self.displayCoords) # Add start and end points - labels = [] - tickLabelsSize = [0., 0.] - - xTickLength, yTickLength = self._tickLength - for (xPixel, yPixel), dataPos, text in self.ticks: - if text is None: - tickScale = 0.5 - else: - tickScale = 1. - - label = Text2D(text=text, - x=xPixel - xTickLength, - y=yPixel - yTickLength, - align=self._labelAlign, - valign=self._labelVAlign) - - width, height = label.size - if width > tickLabelsSize[0]: - tickLabelsSize[0] = width - if height > tickLabelsSize[1]: - tickLabelsSize[1] = height - - labels.append(label) - - vertices.append((xPixel, yPixel)) - vertices.append((xPixel + tickScale * xTickLength, - yPixel + tickScale * yTickLength)) - - (x0, y0), (x1, y1) = self.displayCoords - xAxisCenter = 0.5 * (x0 + x1) - yAxisCenter = 0.5 * (y0 + y1) - - xOffset, yOffset = self._titleOffset - - # Adaptative title positioning: - # tickNorm = math.sqrt(xTickLength ** 2 + yTickLength ** 2) - # xOffset = -tickLabelsSize[0] * xTickLength / tickNorm - # xOffset -= 3 * xTickLength - # yOffset = -tickLabelsSize[1] * yTickLength / tickNorm - # yOffset -= 3 * yTickLength - - axisTitle = Text2D(text=self.title, - x=xAxisCenter + xOffset, - y=yAxisCenter + yOffset, - align=self._titleAlign, - valign=self._titleVAlign, - rotate=self._titleRotate) - labels.append(axisTitle) - - return vertices, labels - - def _dirtyTicks(self): - """Mark ticks as dirty and notify listener (i.e., background).""" - self._ticks = None - plot = self._plot() - if plot is not None: - plot._dirty() - - @staticmethod - def _frange(start, stop, step): - """range for float (including stop).""" - while start <= stop: - yield start - start += step - - def _ticksGenerator(self): - """Generator of ticks as tuples: - ((x, y) in display, dataPos, textLabel). - """ - dataMin, dataMax = self.dataRange - if self.isLog and dataMin <= 0.: - _logger.warning( - 'Getting ticks while isLog=True and dataRange[0]<=0.') - dataMin = 1. - if dataMax < dataMin: - dataMax = 1. - - if dataMin != dataMax: # data range is not null - (x0, y0), (x1, y1) = self.displayCoords - - if self.isLog: - - if self.isTimeSeries: - _logger.warning("Time series not implemented for log-scale") - - logMin, logMax = math.log10(dataMin), math.log10(dataMax) - tickMin, tickMax, step, _ = niceNumbersForLog10(logMin, logMax) - - xScale = (x1 - x0) / (logMax - logMin) - yScale = (y1 - y0) / (logMax - logMin) - - for logPos in self._frange(tickMin, tickMax, step): - if logMin <= logPos <= logMax: - dataPos = 10 ** logPos - xPixel = x0 + (logPos - logMin) * xScale - yPixel = y0 + (logPos - logMin) * yScale - text = '1e%+03d' % logPos - yield ((xPixel, yPixel), dataPos, text) - - if step == 1: - ticks = list(self._frange(tickMin, tickMax, step))[:-1] - for logPos in ticks: - dataOrigPos = 10 ** logPos - for index in range(2, 10): - dataPos = dataOrigPos * index - if dataMin <= dataPos <= dataMax: - logSubPos = math.log10(dataPos) - xPixel = x0 + (logSubPos - logMin) * xScale - yPixel = y0 + (logSubPos - logMin) * yScale - yield ((xPixel, yPixel), dataPos, None) - - else: - xScale = (x1 - x0) / (dataMax - dataMin) - yScale = (y1 - y0) / (dataMax - dataMin) - - nbPixels = math.sqrt(pow(x1 - x0, 2) + pow(y1 - y0, 2)) - - # Density of 1.3 label per 92 pixels - # i.e., 1.3 label per inch on a 92 dpi screen - tickDensity = 1.3 / 92 - - if not self.isTimeSeries: - tickMin, tickMax, step, nbFrac = niceNumbersAdaptative( - dataMin, dataMax, nbPixels, tickDensity) - - for dataPos in self._frange(tickMin, tickMax, step): - if dataMin <= dataPos <= dataMax: - xPixel = x0 + (dataPos - dataMin) * xScale - yPixel = y0 + (dataPos - dataMin) * yScale - - if nbFrac == 0: - text = '%g' % dataPos - else: - text = ('%.' + str(nbFrac) + 'f') % dataPos - yield ((xPixel, yPixel), dataPos, text) - else: - # Time series - dtMin = dt.datetime.fromtimestamp(dataMin, tz=self.timeZone) - dtMax = dt.datetime.fromtimestamp(dataMax, tz=self.timeZone) - - tickDateTimes, spacing, unit = calcTicksAdaptive( - dtMin, dtMax, nbPixels, tickDensity) - - for tickDateTime in tickDateTimes: - if dtMin <= tickDateTime <= dtMax: - - dataPos = timestamp(tickDateTime) - xPixel = x0 + (dataPos - dataMin) * xScale - yPixel = y0 + (dataPos - dataMin) * yScale - - fmtStr = bestFormatString(spacing, unit) - text = tickDateTime.strftime(fmtStr) - - yield ((xPixel, yPixel), dataPos, text) - - -# GLPlotFrame ################################################################# - -class GLPlotFrame(object): - """Base class for rendering a 2D frame surrounded by axes.""" - - _TICK_LENGTH_IN_PIXELS = 5 - _LINE_WIDTH = 1 - - _SHADERS = { - 'vertex': """ - attribute vec2 position; - uniform mat4 matrix; - - void main(void) { - gl_Position = matrix * vec4(position, 0.0, 1.0); - } - """, - 'fragment': """ - uniform vec4 color; - uniform float tickFactor; /* = 1./tickLength or 0. for solid line */ - - void main(void) { - if (mod(tickFactor * (gl_FragCoord.x + gl_FragCoord.y), 2.) < 1.) { - gl_FragColor = color; - } else { - discard; - } - } - """ - } - - _Margins = namedtuple('Margins', ('left', 'right', 'top', 'bottom')) - - # Margins used when plot frame is not displayed - _NoDisplayMargins = _Margins(0, 0, 0, 0) - - def __init__(self, margins): - """ - :param margins: The margins around plot area for axis and labels. - :type margins: dict with 'left', 'right', 'top', 'bottom' keys and - values as ints. - """ - self._renderResources = None - - self._margins = self._Margins(**margins) - - self.axes = [] # List of PlotAxis to be updated by subclasses - - self._grid = False - self._size = 0., 0. - self._title = '' - self._displayed = True - - @property - def isDirty(self): - """True if it need to refresh graphic rendering, False otherwise.""" - return self._renderResources is None - - GRID_NONE = 0 - GRID_MAIN_TICKS = 1 - GRID_SUB_TICKS = 2 - GRID_ALL_TICKS = (GRID_MAIN_TICKS + GRID_SUB_TICKS) - - @property - def displayed(self): - """Whether axes and their labels are displayed or not (bool)""" - return self._displayed - - @displayed.setter - def displayed(self, displayed): - displayed = bool(displayed) - if displayed != self._displayed: - self._displayed = displayed - self._dirty() - - @property - def margins(self): - """Margins in pixels around the plot.""" - if not self.displayed: - return self._NoDisplayMargins - else: - return self._margins - - @property - def grid(self): - """Grid display mode: - - 0: No grid. - - 1: Grid on main ticks. - - 2: Grid on sub-ticks for log scale axes. - - 3: Grid on main and sub ticks.""" - return self._grid - - @grid.setter - def grid(self, grid): - assert grid in (self.GRID_NONE, self.GRID_MAIN_TICKS, - self.GRID_SUB_TICKS, self.GRID_ALL_TICKS) - if grid != self._grid: - self._grid = grid - self._dirty() - - @property - def size(self): - """Size in pixels of the plot area including margins.""" - return self._size - - @size.setter - def size(self, size): - assert len(size) == 2 - size = tuple(size) - if size != self._size: - self._size = size - self._dirty() - - @property - def plotOrigin(self): - """Plot area origin (left, top) in widget coordinates in pixels.""" - return self.margins.left, self.margins.top - - @property - def plotSize(self): - """Plot area size (width, height) in pixels.""" - w, h = self.size - w -= self.margins.left + self.margins.right - h -= self.margins.top + self.margins.bottom - return w, h - - @property - def title(self): - """Main title as a str in latin-1.""" - return self._title - - @title.setter - def title(self, title): - if title != self._title: - self._title = title - self._dirty() - - # In-place update - # if self._renderResources is not None: - # self._renderResources[-1][-1].text = title - - def _dirty(self): - # When Text2D require discard we need to handle it - self._renderResources = None - - def _buildGridVertices(self): - if self._grid == self.GRID_NONE: - return [] - - elif self._grid == self.GRID_MAIN_TICKS: - def test(text): - return text is not None - elif self._grid == self.GRID_SUB_TICKS: - def test(text): - return text is None - elif self._grid == self.GRID_ALL_TICKS: - def test(_): - return True - else: - logging.warning('Wrong grid mode: %d' % self._grid) - return [] - - return self._buildGridVerticesWithTest(test) - - def _buildGridVerticesWithTest(self, test): - """Override in subclass to generate grid vertices""" - return [] - - def _buildVerticesAndLabels(self): - # To fill with copy of axes lists - vertices = [] - labels = [] - - for axis in self.axes: - axisVertices, axisLabels = axis.getVerticesAndLabels() - vertices += axisVertices - labels += axisLabels - - vertices = numpy.array(vertices, dtype=numpy.float32) - - # Add main title - xTitle = (self.size[0] + self.margins.left - - self.margins.right) // 2 - yTitle = self.margins.top - self._TICK_LENGTH_IN_PIXELS - labels.append(Text2D(text=self.title, - x=xTitle, - y=yTitle, - align=CENTER, - valign=BOTTOM)) - - # grid - gridVertices = numpy.array(self._buildGridVertices(), - dtype=numpy.float32) - - self._renderResources = (vertices, gridVertices, labels) - - _program = Program( - _SHADERS['vertex'], _SHADERS['fragment'], attrib0='position') - - def render(self): - if not self.displayed: - return - - if self._renderResources is None: - self._buildVerticesAndLabels() - vertices, gridVertices, labels = self._renderResources - - width, height = self.size - matProj = mat4Ortho(0, width, height, 0, 1, -1) - - gl.glViewport(0, 0, width, height) - - prog = self._program - prog.use() - - gl.glLineWidth(self._LINE_WIDTH) - - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - matProj.astype(numpy.float32)) - gl.glUniform4f(prog.uniforms['color'], 0., 0., 0., 1.) - gl.glUniform1f(prog.uniforms['tickFactor'], 0.) - - gl.glEnableVertexAttribArray(prog.attributes['position']) - gl.glVertexAttribPointer(prog.attributes['position'], - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, vertices) - - gl.glDrawArrays(gl.GL_LINES, 0, len(vertices)) - - for label in labels: - label.render(matProj) - - def renderGrid(self): - if self._grid == self.GRID_NONE: - return - - if self._renderResources is None: - self._buildVerticesAndLabels() - vertices, gridVertices, labels = self._renderResources - - width, height = self.size - matProj = mat4Ortho(0, width, height, 0, 1, -1) - - gl.glViewport(0, 0, width, height) - - prog = self._program - prog.use() - - gl.glLineWidth(self._LINE_WIDTH) - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - matProj.astype(numpy.float32)) - gl.glUniform4f(prog.uniforms['color'], 0.7, 0.7, 0.7, 1.) - gl.glUniform1f(prog.uniforms['tickFactor'], 0.) # 1/2.) # 1/tickLen - - gl.glEnableVertexAttribArray(prog.attributes['position']) - gl.glVertexAttribPointer(prog.attributes['position'], - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, gridVertices) - - gl.glDrawArrays(gl.GL_LINES, 0, len(gridVertices)) - - -# GLPlotFrame2D ############################################################### - -class GLPlotFrame2D(GLPlotFrame): - def __init__(self, margins): - """ - :param margins: The margins around plot area for axis and labels. - :type margins: dict with 'left', 'right', 'top', 'bottom' keys and - values as ints. - """ - super(GLPlotFrame2D, self).__init__(margins) - self.axes.append(PlotAxis(self, - tickLength=(0., -5.), - labelAlign=CENTER, labelVAlign=TOP, - titleAlign=CENTER, titleVAlign=TOP, - titleRotate=0, - titleOffset=(0, self.margins.bottom // 2))) - - self._x2AxisCoords = () - - self.axes.append(PlotAxis(self, - tickLength=(5., 0.), - labelAlign=RIGHT, labelVAlign=CENTER, - titleAlign=CENTER, titleVAlign=BOTTOM, - titleRotate=ROTATE_270, - titleOffset=(-3 * self.margins.left // 4, - 0))) - - self._y2Axis = PlotAxis(self, - tickLength=(-5., 0.), - labelAlign=LEFT, labelVAlign=CENTER, - titleAlign=CENTER, titleVAlign=TOP, - titleRotate=ROTATE_270, - titleOffset=(3 * self.margins.right // 4, - 0)) - - self._isYAxisInverted = False - - self._dataRanges = { - 'x': (1., 100.), 'y': (1., 100.), 'y2': (1., 100.)} - - self._baseVectors = (1., 0.), (0., 1.) - - self._transformedDataRanges = None - self._transformedDataProjMat = None - self._transformedDataY2ProjMat = None - - def _dirty(self): - super(GLPlotFrame2D, self)._dirty() - self._transformedDataRanges = None - self._transformedDataProjMat = None - self._transformedDataY2ProjMat = None - - @property - def isDirty(self): - """True if it need to refresh graphic rendering, False otherwise.""" - return (super(GLPlotFrame2D, self).isDirty or - self._transformedDataRanges is None or - self._transformedDataProjMat is None or - self._transformedDataY2ProjMat is None) - - @property - def xAxis(self): - return self.axes[0] - - @property - def yAxis(self): - return self.axes[1] - - @property - def y2Axis(self): - return self._y2Axis - - @property - def isY2Axis(self): - """Whether to display the left Y axis or not.""" - return len(self.axes) == 3 - - @isY2Axis.setter - def isY2Axis(self, isY2Axis): - if isY2Axis != self.isY2Axis: - if isY2Axis: - self.axes.append(self._y2Axis) - else: - self.axes = self.axes[:2] - - self._dirty() - - @property - def isYAxisInverted(self): - """Whether Y axes are inverted or not as a bool.""" - return self._isYAxisInverted - - @isYAxisInverted.setter - def isYAxisInverted(self, value): - value = bool(value) - if value != self._isYAxisInverted: - self._isYAxisInverted = value - self._dirty() - - DEFAULT_BASE_VECTORS = (1., 0.), (0., 1.) - """Values of baseVectors for orthogonal axes.""" - - @property - def baseVectors(self): - """Coordinates of the X and Y axes in the orthogonal plot coords. - - Raises ValueError if corresponding matrix is singular. - - 2 tuples of 2 floats: (xx, xy), (yx, yy) - """ - return self._baseVectors - - @baseVectors.setter - def baseVectors(self, baseVectors): - self._dirty() - - (xx, xy), (yx, yy) = baseVectors - vectors = (float(xx), float(xy)), (float(yx), float(yy)) - - det = (vectors[0][0] * vectors[1][1] - vectors[1][0] * vectors[0][1]) - if det == 0.: - raise ValueError("Singular matrix for base vectors: " + - str(vectors)) - - if vectors != self._baseVectors: - self._baseVectors = vectors - self._dirty() - - @property - def dataRanges(self): - """Ranges of data visible in the plot on x, y and y2 axes. - - This is different to the axes range when axes are not orthogonal. - - Type: ((xMin, xMax), (yMin, yMax), (y2Min, y2Max)) - """ - return self._DataRanges(self._dataRanges['x'], - self._dataRanges['y'], - self._dataRanges['y2']) - - @staticmethod - def _clipToSafeRange(min_, max_, isLog): - # Clip range if needed - minLimit = FLOAT32_MINPOS if isLog else FLOAT32_SAFE_MIN - min_ = numpy.clip(min_, minLimit, FLOAT32_SAFE_MAX) - max_ = numpy.clip(max_, minLimit, FLOAT32_SAFE_MAX) - assert min_ < max_ - return min_, max_ - - def setDataRanges(self, x=None, y=None, y2=None): - """Set data range over each axes. - - The provided ranges are clipped to possible values - (i.e., 32 float range + positive range for log scale). - - :param x: (min, max) data range over X axis - :param y: (min, max) data range over Y axis - :param y2: (min, max) data range over Y2 axis - """ - if x is not None: - self._dataRanges['x'] = \ - self._clipToSafeRange(x[0], x[1], self.xAxis.isLog) - - if y is not None: - self._dataRanges['y'] = \ - self._clipToSafeRange(y[0], y[1], self.yAxis.isLog) - - if y2 is not None: - self._dataRanges['y2'] = \ - self._clipToSafeRange(y2[0], y2[1], self.y2Axis.isLog) - - self.xAxis.dataRange = self._dataRanges['x'] - self.yAxis.dataRange = self._dataRanges['y'] - self.y2Axis.dataRange = self._dataRanges['y2'] - - _DataRanges = namedtuple('dataRanges', ('x', 'y', 'y2')) - - @property - def transformedDataRanges(self): - """Bounds of the displayed area in transformed data coordinates - (i.e., log scale applied if any as well as skew) - - 3-tuple of 2-tuple (min, max) for each axis: x, y, y2. - """ - if self._transformedDataRanges is None: - (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = self.dataRanges - - if self.xAxis.isLog: - try: - xMin = math.log10(xMin) - except ValueError: - _logger.info('xMin: warning log10(%f)', xMin) - xMin = 0. - try: - xMax = math.log10(xMax) - except ValueError: - _logger.info('xMax: warning log10(%f)', xMax) - xMax = 0. - - if self.yAxis.isLog: - try: - yMin = math.log10(yMin) - except ValueError: - _logger.info('yMin: warning log10(%f)', yMin) - yMin = 0. - try: - yMax = math.log10(yMax) - except ValueError: - _logger.info('yMax: warning log10(%f)', yMax) - yMax = 0. - - try: - y2Min = math.log10(y2Min) - except ValueError: - _logger.info('yMin: warning log10(%f)', y2Min) - y2Min = 0. - try: - y2Max = math.log10(y2Max) - except ValueError: - _logger.info('yMax: warning log10(%f)', y2Max) - y2Max = 0. - - # Non-orthogonal axes - if self.baseVectors != self.DEFAULT_BASE_VECTORS: - (xx, xy), (yx, yy) = self.baseVectors - skew_mat = numpy.array(((xx, yx), (xy, yy))) - - corners = [(xMin, yMin), (xMin, yMax), - (xMax, yMin), (xMax, yMax), - (xMin, y2Min), (xMin, y2Max), - (xMax, y2Min), (xMax, y2Max)] - - corners = numpy.array( - [numpy.dot(skew_mat, corner) for corner in corners], - dtype=numpy.float32) - xMin, xMax = corners[:, 0].min(), corners[:, 0].max() - yMin, yMax = corners[0:4, 1].min(), corners[0:4, 1].max() - y2Min, y2Max = corners[4:, 1].min(), corners[4:, 1].max() - - self._transformedDataRanges = self._DataRanges( - (xMin, xMax), (yMin, yMax), (y2Min, y2Max)) - - return self._transformedDataRanges - - @property - def transformedDataProjMat(self): - """Orthographic projection matrix for rendering transformed data - - :type: numpy.matrix - """ - if self._transformedDataProjMat is None: - xMin, xMax = self.transformedDataRanges.x - yMin, yMax = self.transformedDataRanges.y - - if self.isYAxisInverted: - mat = mat4Ortho(xMin, xMax, yMax, yMin, 1, -1) - else: - mat = mat4Ortho(xMin, xMax, yMin, yMax, 1, -1) - - # Non-orthogonal axes - if self.baseVectors != self.DEFAULT_BASE_VECTORS: - (xx, xy), (yx, yy) = self.baseVectors - mat = numpy.dot(mat, numpy.array(( - (xx, yx, 0., 0.), - (xy, yy, 0., 0.), - (0., 0., 1., 0.), - (0., 0., 0., 1.)), dtype=numpy.float64)) - - self._transformedDataProjMat = mat - - return self._transformedDataProjMat - - @property - def transformedDataY2ProjMat(self): - """Orthographic projection matrix for rendering transformed data - for the 2nd Y axis - - :type: numpy.matrix - """ - if self._transformedDataY2ProjMat is None: - xMin, xMax = self.transformedDataRanges.x - y2Min, y2Max = self.transformedDataRanges.y2 - - if self.isYAxisInverted: - mat = mat4Ortho(xMin, xMax, y2Max, y2Min, 1, -1) - else: - mat = mat4Ortho(xMin, xMax, y2Min, y2Max, 1, -1) - - # Non-orthogonal axes - if self.baseVectors != self.DEFAULT_BASE_VECTORS: - (xx, xy), (yx, yy) = self.baseVectors - mat = numpy.dot(mat, numpy.matrix(( - (xx, yx, 0., 0.), - (xy, yy, 0., 0.), - (0., 0., 1., 0.), - (0., 0., 0., 1.)), dtype=numpy.float64)) - - self._transformedDataY2ProjMat = mat - - return self._transformedDataY2ProjMat - - def dataToPixel(self, x, y, axis='left'): - """Convert data coordinate to widget pixel coordinate. - """ - assert axis in ('left', 'right') - - trBounds = self.transformedDataRanges - - if self.xAxis.isLog: - if x < FLOAT32_MINPOS: - return None - xDataTr = math.log10(x) - else: - xDataTr = x - - if self.yAxis.isLog: - if y < FLOAT32_MINPOS: - return None - yDataTr = math.log10(y) - else: - yDataTr = y - - # Non-orthogonal axes - if self.baseVectors != self.DEFAULT_BASE_VECTORS: - (xx, xy), (yx, yy) = self.baseVectors - skew_mat = numpy.array(((xx, yx), (xy, yy))) - - coords = numpy.dot(skew_mat, numpy.array((xDataTr, yDataTr))) - xDataTr, yDataTr = coords - - plotWidth, plotHeight = self.plotSize - - xPixel = int(self.margins.left + - plotWidth * (xDataTr - trBounds.x[0]) / - (trBounds.x[1] - trBounds.x[0])) - - usedAxis = trBounds.y if axis == "left" else trBounds.y2 - yOffset = (plotHeight * (yDataTr - usedAxis[0]) / - (usedAxis[1] - usedAxis[0])) - - if self.isYAxisInverted: - yPixel = int(self.margins.top + yOffset) - else: - yPixel = int(self.size[1] - self.margins.bottom - yOffset) - - return xPixel, yPixel - - def pixelToData(self, x, y, axis="left"): - """Convert pixel position to data coordinates. - - :param float x: X coord - :param float y: Y coord - :param str axis: Y axis to use in ('left', 'right') - :return: (x, y) position in data coords - """ - assert axis in ("left", "right") - - plotWidth, plotHeight = self.plotSize - - trBounds = self.transformedDataRanges - - xData = (x - self.margins.left + 0.5) / float(plotWidth) - xData = trBounds.x[0] + xData * (trBounds.x[1] - trBounds.x[0]) - - usedAxis = trBounds.y if axis == "left" else trBounds.y2 - if self.isYAxisInverted: - yData = (y - self.margins.top + 0.5) / float(plotHeight) - yData = usedAxis[0] + yData * (usedAxis[1] - usedAxis[0]) - else: - yData = self.size[1] - self.margins.bottom - y - 0.5 - yData /= float(plotHeight) - yData = usedAxis[0] + yData * (usedAxis[1] - usedAxis[0]) - - # non-orthogonal axis - if self.baseVectors != self.DEFAULT_BASE_VECTORS: - (xx, xy), (yx, yy) = self.baseVectors - skew_mat = numpy.array(((xx, yx), (xy, yy))) - skew_mat = numpy.linalg.inv(skew_mat) - - coords = numpy.dot(skew_mat, numpy.array((xData, yData))) - xData, yData = coords - - if self.xAxis.isLog: - xData = pow(10, xData) - if self.yAxis.isLog: - yData = pow(10, yData) - - return xData, yData - - def _buildGridVerticesWithTest(self, test): - vertices = [] - - if self.baseVectors == self.DEFAULT_BASE_VECTORS: - for axis in self.axes: - for (xPixel, yPixel), data, text in axis.ticks: - if test(text): - vertices.append((xPixel, yPixel)) - if axis == self.xAxis: - vertices.append((xPixel, self.margins.top)) - elif axis == self.yAxis: - vertices.append((self.size[0] - self.margins.right, - yPixel)) - else: # axis == self.y2Axis - vertices.append((self.margins.left, yPixel)) - - else: - # Get plot corners in data coords - plotLeft, plotTop = self.plotOrigin - plotWidth, plotHeight = self.plotSize - - corners = [(plotLeft, plotTop), - (plotLeft, plotTop + plotHeight), - (plotLeft + plotWidth, plotTop + plotHeight), - (plotLeft + plotWidth, plotTop)] - - for axis in self.axes: - if axis == self.xAxis: - cornersInData = numpy.array([ - self.pixelToData(x, y) for (x, y) in corners]) - borders = ((cornersInData[0], cornersInData[3]), # top - (cornersInData[1], cornersInData[0]), # left - (cornersInData[3], cornersInData[2])) # right - - for (xPixel, yPixel), data, text in axis.ticks: - if test(text): - for (x0, y0), (x1, y1) in borders: - if min(x0, x1) <= data < max(x0, x1): - yIntersect = (data - x0) * \ - (y1 - y0) / (x1 - x0) + y0 - - pixelPos = self.dataToPixel( - data, yIntersect) - if pixelPos is not None: - vertices.append((xPixel, yPixel)) - vertices.append(pixelPos) - break # Stop at first intersection - - else: # y or y2 axes - if axis == self.yAxis: - axis_name = 'left' - cornersInData = numpy.array([ - self.pixelToData(x, y) for (x, y) in corners]) - borders = ( - (cornersInData[3], cornersInData[2]), # right - (cornersInData[0], cornersInData[3]), # top - (cornersInData[2], cornersInData[1])) # bottom - - else: # axis == self.y2Axis - axis_name = 'right' - corners = numpy.array([self.pixelToData( - x, y, axis='right') for (x, y) in corners]) - borders = ( - (cornersInData[1], cornersInData[0]), # left - (cornersInData[0], cornersInData[3]), # top - (cornersInData[2], cornersInData[1])) # bottom - - for (xPixel, yPixel), data, text in axis.ticks: - if test(text): - for (x0, y0), (x1, y1) in borders: - if min(y0, y1) <= data < max(y0, y1): - xIntersect = (data - y0) * \ - (x1 - x0) / (y1 - y0) + x0 - - pixelPos = self.dataToPixel( - xIntersect, data, axis=axis_name) - if pixelPos is not None: - vertices.append((xPixel, yPixel)) - vertices.append(pixelPos) - break # Stop at first intersection - - return vertices - - def _buildVerticesAndLabels(self): - width, height = self.size - - xCoords = (self.margins.left - 0.5, - width - self.margins.right + 0.5) - yCoords = (height - self.margins.bottom + 0.5, - self.margins.top - 0.5) - - self.axes[0].displayCoords = ((xCoords[0], yCoords[0]), - (xCoords[1], yCoords[0])) - - self._x2AxisCoords = ((xCoords[0], yCoords[1]), - (xCoords[1], yCoords[1])) - - if self.isYAxisInverted: - # Y axes are inverted, axes coordinates are inverted - yCoords = yCoords[1], yCoords[0] - - self.axes[1].displayCoords = ((xCoords[0], yCoords[0]), - (xCoords[0], yCoords[1])) - - self._y2Axis.displayCoords = ((xCoords[1], yCoords[0]), - (xCoords[1], yCoords[1])) - - super(GLPlotFrame2D, self)._buildVerticesAndLabels() - - vertices, gridVertices, labels = self._renderResources - - # Adds vertices for borders without axis - extraVertices = [] - extraVertices += self._x2AxisCoords - if not self.isY2Axis: - extraVertices += self._y2Axis.displayCoords - - extraVertices = numpy.array( - extraVertices, copy=False, dtype=numpy.float32) - vertices = numpy.append(vertices, extraVertices, axis=0) - - self._renderResources = (vertices, gridVertices, labels) diff --git a/silx/gui/plot/backends/glutils/GLPlotImage.py b/silx/gui/plot/backends/glutils/GLPlotImage.py deleted file mode 100644 index 6f3c487..0000000 --- a/silx/gui/plot/backends/glutils/GLPlotImage.py +++ /dev/null @@ -1,674 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -""" -This module provides a class to render 2D array as a colormap or RGB(A) image -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -import math -import numpy - -from silx.math.combo import min_max - -from ...._glutils import gl, Program, Texture -from ..._utils import FLOAT32_MINPOS -from .GLSupport import mat4Translate, mat4Scale -from .GLTexture import Image - - -class _GLPlotData2D(object): - def __init__(self, data, origin, scale): - self.data = data - assert len(origin) == 2 - self.origin = tuple(origin) - assert len(scale) == 2 - self.scale = tuple(scale) - - def pick(self, x, y): - if self.xMin <= x <= self.xMax and self.yMin <= y <= self.yMax: - ox, oy = self.origin - sx, sy = self.scale - col = int((x - ox) / sx) - row = int((y - oy) / sy) - return col, row - else: - return None - - @property - def xMin(self): - ox, sx = self.origin[0], self.scale[0] - return ox if sx >= 0. else ox + sx * self.data.shape[1] - - @property - def yMin(self): - oy, sy = self.origin[1], self.scale[1] - return oy if sy >= 0. else oy + sy * self.data.shape[0] - - @property - def xMax(self): - ox, sx = self.origin[0], self.scale[0] - return ox + sx * self.data.shape[1] if sx >= 0. else ox - - @property - def yMax(self): - oy, sy = self.origin[1], self.scale[1] - return oy + sy * self.data.shape[0] if sy >= 0. else oy - - def discard(self): - pass - - def prepare(self): - pass - - def render(self, matrix, isXLog, isYLog): - pass - - -class GLPlotColormap(_GLPlotData2D): - - _SHADERS = { - 'linear': { - 'vertex': """ - #version 120 - - uniform mat4 matrix; - attribute vec2 texCoords; - attribute vec2 position; - - varying vec2 coords; - - void main(void) { - coords = texCoords; - gl_Position = matrix * vec4(position, 0.0, 1.0); - } - """, - 'fragTransform': """ - vec2 textureCoords(void) { - return coords; - } - """}, - - 'log': { - 'vertex': """ - #version 120 - - attribute vec2 position; - uniform mat4 matrix; - uniform mat4 matOffset; - uniform bvec2 isLog; - - varying vec2 coords; - - const float oneOverLog10 = 0.43429448190325176; - - void main(void) { - vec4 dataPos = matOffset * vec4(position, 0.0, 1.0); - if (isLog.x) { - dataPos.x = oneOverLog10 * log(dataPos.x); - } - if (isLog.y) { - dataPos.y = oneOverLog10 * log(dataPos.y); - } - coords = dataPos.xy; - gl_Position = matrix * dataPos; - } - """, - 'fragTransform': """ - uniform bvec2 isLog; - uniform struct { - vec2 oneOverRange; - vec2 originOverRange; - } bounds; - - vec2 textureCoords(void) { - vec2 pos = coords; - if (isLog.x) { - pos.x = pow(10., coords.x); - } - if (isLog.y) { - pos.y = pow(10., coords.y); - } - return pos * bounds.oneOverRange - bounds.originOverRange; - // TODO texture coords in range different from [0, 1] - } - """}, - - 'fragment': """ - #version 120 - - uniform sampler2D data; - uniform struct { - sampler2D texture; - bool isLog; - float min; - float oneOverRange; - } cmap; - uniform float alpha; - - varying vec2 coords; - - %s - - const float oneOverLog10 = 0.43429448190325176; - - void main(void) { - float value = texture2D(data, textureCoords()).r; - if (cmap.isLog) { - if (value > 0.) { - value = clamp(cmap.oneOverRange * - (oneOverLog10 * log(value) - cmap.min), - 0., 1.); - } else { - value = 0.; - } - } else { /*Linear mapping*/ - value = clamp(cmap.oneOverRange * (value - cmap.min), 0., 1.); - } - - gl_FragColor = texture2D(cmap.texture, vec2(value, 0.5)); - gl_FragColor.a *= alpha; - } - """ - } - - _DATA_TEX_UNIT = 0 - _CMAP_TEX_UNIT = 1 - - _INTERNAL_FORMATS = { - numpy.dtype(numpy.float32): gl.GL_R32F, - # Use normalized integer for unsigned int formats - numpy.dtype(numpy.uint16): gl.GL_R16, - numpy.dtype(numpy.uint8): gl.GL_R8, - } - - _linearProgram = Program(_SHADERS['linear']['vertex'], - _SHADERS['fragment'] % - _SHADERS['linear']['fragTransform'], - attrib0='position') - - _logProgram = Program(_SHADERS['log']['vertex'], - _SHADERS['fragment'] % - _SHADERS['log']['fragTransform'], - attrib0='position') - - def __init__(self, data, origin, scale, - colormap, cmapIsLog=False, cmapRange=None, - alpha=1.0): - """Create a 2D colormap - - :param data: The 2D scalar data array to display - :type data: numpy.ndarray with 2 dimensions (dtype=numpy.float32) - :param origin: (x, y) coordinates of the origin of the data array - :type origin: 2-tuple of floats. - :param scale: (sx, sy) scale factors of the data array. - This is the size of a data pixel in plot data space. - :type scale: 2-tuple of floats. - :param str colormap: Name of the colormap to use - TODO: Accept a 1D scalar array as the colormap - :param bool cmapIsLog: If True, uses log10 of the data value - :param cmapRange: The range of colormap or None for autoscale colormap - For logarithmic colormap, the range is in the untransformed data - TODO: check consistency with matplotlib - :type cmapRange: (float, float) or None - :param float alpha: Opacity from 0 (transparent) to 1 (opaque) - """ - assert data.dtype in self._INTERNAL_FORMATS - - super(GLPlotColormap, self).__init__(data, origin, scale) - self.colormap = numpy.array(colormap, copy=False) - self.cmapIsLog = cmapIsLog - self._cmapRange = (1., 10.) # Colormap range - self.cmapRange = cmapRange # Update _cmapRange - self._alpha = numpy.clip(alpha, 0., 1.) - - self._cmap_texture = None - self._texture = None - self._textureIsDirty = False - - def discard(self): - if self._cmap_texture is not None: - self._cmap_texture.discard() - self._cmap_texture = None - - if self._texture is not None: - self._texture.discard() - self._texture = None - self._textureIsDirty = False - - @property - def cmapRange(self): - if self.cmapIsLog: - assert self._cmapRange[0] > 0. and self._cmapRange[1] > 0. - return self._cmapRange - - @cmapRange.setter - def cmapRange(self, cmapRange): - assert len(cmapRange) == 2 - assert cmapRange[0] <= cmapRange[1] - self._cmapRange = float(cmapRange[0]), float(cmapRange[1]) - - @property - def alpha(self): - return self._alpha - - def updateData(self, data): - assert data.dtype in self._INTERNAL_FORMATS - oldData = self.data - self.data = data - - if self._texture is not None: - if (self.data.shape != oldData.shape or - self.data.dtype != oldData.dtype): - self.discard() - else: - self._textureIsDirty = True - - def prepare(self): - if self._cmap_texture is None: - # TODO share cmap texture accross Images - # put all cmaps in one texture - colormap = numpy.empty((16, 256, self.colormap.shape[1]), - dtype=self.colormap.dtype) - colormap[:] = self.colormap - format_ = gl.GL_RGBA if colormap.shape[-1] == 4 else gl.GL_RGB - self._cmap_texture = Texture(internalFormat=format_, - data=colormap, - format_=format_, - texUnit=self._CMAP_TEX_UNIT, - minFilter=gl.GL_NEAREST, - magFilter=gl.GL_NEAREST, - wrap=(gl.GL_CLAMP_TO_EDGE, - gl.GL_CLAMP_TO_EDGE)) - - if self._texture is None: - internalFormat = self._INTERNAL_FORMATS[self.data.dtype] - - self._texture = Image(internalFormat, - self.data, - format_=gl.GL_RED, - texUnit=self._DATA_TEX_UNIT) - elif self._textureIsDirty: - self._textureIsDirty = True - self._texture.updateAll(format_=gl.GL_RED, data=self.data) - - def _setCMap(self, prog): - dataMin, dataMax = self.cmapRange # If log, it is stricly positive - - if self.data.dtype in (numpy.uint16, numpy.uint8): - # Using unsigned int as normalized integer in OpenGL - # So normalize range - maxInt = float(numpy.iinfo(self.data.dtype).max) - dataMin, dataMax = dataMin / maxInt, dataMax / maxInt - - if self.cmapIsLog: - dataMin = math.log10(dataMin) - dataMax = math.log10(dataMax) - - gl.glUniform1i(prog.uniforms['cmap.texture'], - self._cmap_texture.texUnit) - gl.glUniform1i(prog.uniforms['cmap.isLog'], self.cmapIsLog) - gl.glUniform1f(prog.uniforms['cmap.min'], dataMin) - if dataMax > dataMin: - oneOverRange = 1. / (dataMax - dataMin) - else: - oneOverRange = 0. # Fall-back - gl.glUniform1f(prog.uniforms['cmap.oneOverRange'], oneOverRange) - - self._cmap_texture.bind() - - def _renderLinear(self, matrix): - self.prepare() - - prog = self._linearProgram - prog.use() - - gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT) - - mat = numpy.dot(numpy.dot(matrix, - mat4Translate(*self.origin)), - mat4Scale(*self.scale)) - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - mat.astype(numpy.float32)) - - gl.glUniform1f(prog.uniforms['alpha'], self.alpha) - - self._setCMap(prog) - - self._texture.render(prog.attributes['position'], - prog.attributes['texCoords'], - self._DATA_TEX_UNIT) - - def _renderLog10(self, matrix, isXLog, isYLog): - xMin, yMin = self.xMin, self.yMin - if ((isXLog and xMin < FLOAT32_MINPOS) or - (isYLog and yMin < FLOAT32_MINPOS)): - # Do not render images that are partly or totally <= 0 - return - - self.prepare() - - prog = self._logProgram - prog.use() - - ox, oy = self.origin - - gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT) - - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - matrix.astype(numpy.float32)) - mat = numpy.dot(mat4Translate(ox, oy), mat4Scale(*self.scale)) - gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE, - mat.astype(numpy.float32)) - - gl.glUniform2i(prog.uniforms['isLog'], isXLog, isYLog) - - ex = ox + self.scale[0] * self.data.shape[1] - ey = oy + self.scale[1] * self.data.shape[0] - - xOneOverRange = 1. / (ex - ox) - yOneOverRange = 1. / (ey - oy) - gl.glUniform2f(prog.uniforms['bounds.originOverRange'], - ox * xOneOverRange, oy * yOneOverRange) - gl.glUniform2f(prog.uniforms['bounds.oneOverRange'], - xOneOverRange, yOneOverRange) - - gl.glUniform1f(prog.uniforms['alpha'], self.alpha) - - self._setCMap(prog) - - try: - tiles = self._texture.tiles - except AttributeError: - raise RuntimeError("No texture, discard has already been called") - if len(tiles) > 1: - raise NotImplementedError( - "Image over multiple textures not supported with log scale") - - texture, vertices, info = tiles[0] - - texture.bind(self._DATA_TEX_UNIT) - - posAttrib = prog.attributes['position'] - stride = vertices.shape[-1] * vertices.itemsize - gl.glEnableVertexAttribArray(posAttrib) - gl.glVertexAttribPointer(posAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - stride, vertices) - - gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices)) - - def render(self, matrix, isXLog, isYLog): - if any((isXLog, isYLog)): - self._renderLog10(matrix, isXLog, isYLog) - else: - self._renderLinear(matrix) - - # Unbind colormap texture - gl.glActiveTexture(gl.GL_TEXTURE0 + self._cmap_texture.texUnit) - gl.glBindTexture(self._cmap_texture.target, 0) - - -# image ####################################################################### - -class GLPlotRGBAImage(_GLPlotData2D): - - _SHADERS = { - 'linear': { - 'vertex': """ - #version 120 - - attribute vec2 position; - attribute vec2 texCoords; - uniform mat4 matrix; - - varying vec2 coords; - - void main(void) { - gl_Position = matrix * vec4(position, 0.0, 1.0); - coords = texCoords; - } - """, - 'fragment': """ - #version 120 - - uniform sampler2D tex; - uniform float alpha; - - varying vec2 coords; - - void main(void) { - gl_FragColor = texture2D(tex, coords); - gl_FragColor.a *= alpha; - } - """}, - - 'log': { - 'vertex': """ - #version 120 - - attribute vec2 position; - uniform mat4 matrix; - uniform mat4 matOffset; - uniform bvec2 isLog; - - varying vec2 coords; - - const float oneOverLog10 = 0.43429448190325176; - - void main(void) { - vec4 dataPos = matOffset * vec4(position, 0.0, 1.0); - if (isLog.x) { - dataPos.x = oneOverLog10 * log(dataPos.x); - } - if (isLog.y) { - dataPos.y = oneOverLog10 * log(dataPos.y); - } - coords = dataPos.xy; - gl_Position = matrix * dataPos; - } - """, - 'fragment': """ - #version 120 - - uniform sampler2D tex; - uniform bvec2 isLog; - uniform struct { - vec2 oneOverRange; - vec2 originOverRange; - } bounds; - uniform float alpha; - - varying vec2 coords; - - vec2 textureCoords(void) { - vec2 pos = coords; - if (isLog.x) { - pos.x = pow(10., coords.x); - } - if (isLog.y) { - pos.y = pow(10., coords.y); - } - return pos * bounds.oneOverRange - bounds.originOverRange; - // TODO texture coords in range different from [0, 1] - } - - void main(void) { - gl_FragColor = texture2D(tex, textureCoords()); - gl_FragColor.a *= alpha; - } - """} - } - - _DATA_TEX_UNIT = 0 - - _SUPPORTED_DTYPES = (numpy.dtype(numpy.float32), - numpy.dtype(numpy.uint8)) - - _linearProgram = Program(_SHADERS['linear']['vertex'], - _SHADERS['linear']['fragment'], - attrib0='position') - - _logProgram = Program(_SHADERS['log']['vertex'], - _SHADERS['log']['fragment'], - attrib0='position') - - def __init__(self, data, origin, scale, alpha): - """Create a 2D RGB(A) image from data - - :param data: The 2D image data array to display - :type data: numpy.ndarray with 3 dimensions - (dtype=numpy.uint8 or numpy.float32) - :param origin: (x, y) coordinates of the origin of the data array - :type origin: 2-tuple of floats. - :param scale: (sx, sy) scale factors of the data array. - This is the size of a data pixel in plot data space. - :type scale: 2-tuple of floats. - :param float alpha: Opacity from 0 (transparent) to 1 (opaque) - """ - assert data.dtype in self._SUPPORTED_DTYPES - super(GLPlotRGBAImage, self).__init__(data, origin, scale) - self._texture = None - self._textureIsDirty = False - self._alpha = numpy.clip(alpha, 0., 1.) - - @property - def alpha(self): - return self._alpha - - def discard(self): - if self._texture is not None: - self._texture.discard() - self._texture = None - self._textureIsDirty = False - - def updateData(self, data): - assert data.dtype in self._SUPPORTED_DTYPES - oldData = self.data - self.data = data - - if self._texture is not None: - if self.data.shape != oldData.shape: - self.discard() - else: - self._textureIsDirty = True - - def prepare(self): - if self._texture is None: - format_ = gl.GL_RGBA if self.data.shape[2] == 4 else gl.GL_RGB - - self._texture = Image(format_, - self.data, - format_=format_, - texUnit=self._DATA_TEX_UNIT) - elif self._textureIsDirty: - self._textureIsDirty = False - - # We should check that internal format is the same - format_ = gl.GL_RGBA if self.data.shape[2] == 4 else gl.GL_RGB - self._texture.updateAll(format_=format_, data=self.data) - - def _renderLinear(self, matrix): - self.prepare() - - prog = self._linearProgram - prog.use() - - gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT) - - mat = numpy.dot(numpy.dot(matrix, mat4Translate(*self.origin)), - mat4Scale(*self.scale)) - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - mat.astype(numpy.float32)) - - gl.glUniform1f(prog.uniforms['alpha'], self.alpha) - - self._texture.render(prog.attributes['position'], - prog.attributes['texCoords'], - self._DATA_TEX_UNIT) - - def _renderLog(self, matrix, isXLog, isYLog): - self.prepare() - - prog = self._logProgram - prog.use() - - ox, oy = self.origin - - gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT) - - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - matrix.astype(numpy.float32)) - mat = numpy.dot(mat4Translate(ox, oy), mat4Scale(*self.scale)) - gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE, - mat.astype(numpy.float32)) - - gl.glUniform2i(prog.uniforms['isLog'], isXLog, isYLog) - - gl.glUniform1f(prog.uniforms['alpha'], self.alpha) - - ex = ox + self.scale[0] * self.data.shape[1] - ey = oy + self.scale[1] * self.data.shape[0] - - xOneOverRange = 1. / (ex - ox) - yOneOverRange = 1. / (ey - oy) - gl.glUniform2f(prog.uniforms['bounds.originOverRange'], - ox * xOneOverRange, oy * yOneOverRange) - gl.glUniform2f(prog.uniforms['bounds.oneOverRange'], - xOneOverRange, yOneOverRange) - - try: - tiles = self._texture.tiles - except AttributeError: - raise RuntimeError("No texture, discard has already been called") - if len(tiles) > 1: - raise NotImplementedError( - "Image over multiple textures not supported with log scale") - - texture, vertices, info = tiles[0] - - texture.bind(self._DATA_TEX_UNIT) - - posAttrib = prog.attributes['position'] - stride = vertices.shape[-1] * vertices.itemsize - gl.glEnableVertexAttribArray(posAttrib) - gl.glVertexAttribPointer(posAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - stride, vertices) - - gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices)) - - def render(self, matrix, isXLog, isYLog): - if any((isXLog, isYLog)): - self._renderLog(matrix, isXLog, isYLog) - else: - self._renderLinear(matrix) diff --git a/silx/gui/plot/backends/glutils/GLSupport.py b/silx/gui/plot/backends/glutils/GLSupport.py deleted file mode 100644 index 18c5eb7..0000000 --- a/silx/gui/plot/backends/glutils/GLSupport.py +++ /dev/null @@ -1,201 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -""" -This module provides convenient classes and functions for OpenGL rendering. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -import numpy - -from ...._glutils import gl - - -def buildFillMaskIndices(nIndices, dtype=None): - """Returns triangle strip indices for rendering a filled polygon mask - - :param int nIndices: Number of points - :param Union[numpy.dtype,None] dtype: - If specified the dtype of the returned indices array - :return: 1D array of indices constructing a triangle strip - :rtype: numpy.ndarray - """ - if dtype is None: - if nIndices <= numpy.iinfo(numpy.uint16).max + 1: - dtype = numpy.uint16 - else: - dtype = numpy.uint32 - - lastIndex = nIndices - 1 - splitIndex = lastIndex // 2 + 1 - indices = numpy.empty(nIndices, dtype=dtype) - indices[::2] = numpy.arange(0, splitIndex, step=1, dtype=dtype) - indices[1::2] = numpy.arange(lastIndex, splitIndex - 1, step=-1, - dtype=dtype) - return indices - - -class Shape2D(object): - _NO_HATCH = 0 - _HATCH_STEP = 20 - - def __init__(self, points, fill='solid', stroke=True, - fillColor=(0., 0., 0., 1.), strokeColor=(0., 0., 0., 1.), - strokeClosed=True): - self.vertices = numpy.array(points, dtype=numpy.float32, copy=False) - self.strokeClosed = strokeClosed - - self._indices = buildFillMaskIndices(len(self.vertices)) - - tVertex = numpy.transpose(self.vertices) - xMin, xMax = min(tVertex[0]), max(tVertex[0]) - yMin, yMax = min(tVertex[1]), max(tVertex[1]) - self.bboxVertices = numpy.array(((xMin, yMin), (xMin, yMax), - (xMax, yMin), (xMax, yMax)), - dtype=numpy.float32) - self._xMin, self._xMax = xMin, xMax - self._yMin, self._yMax = yMin, yMax - - self.fill = fill - self.fillColor = fillColor - self.stroke = stroke - self.strokeColor = strokeColor - - @property - def xMin(self): - return self._xMin - - @property - def xMax(self): - return self._xMax - - @property - def yMin(self): - return self._yMin - - @property - def yMax(self): - return self._yMax - - def prepareFillMask(self, posAttrib): - gl.glEnableVertexAttribArray(posAttrib) - gl.glVertexAttribPointer(posAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, self.vertices) - - gl.glEnable(gl.GL_STENCIL_TEST) - gl.glStencilMask(1) - gl.glStencilFunc(gl.GL_ALWAYS, 1, 1) - gl.glStencilOp(gl.GL_INVERT, gl.GL_INVERT, gl.GL_INVERT) - gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE) - gl.glDepthMask(gl.GL_FALSE) - - gl.glDrawElements(gl.GL_TRIANGLE_STRIP, len(self._indices), - gl.GL_UNSIGNED_SHORT, self._indices) - - gl.glStencilFunc(gl.GL_EQUAL, 1, 1) - # Reset stencil while drawing - gl.glStencilOp(gl.GL_ZERO, gl.GL_ZERO, gl.GL_ZERO) - gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE) - gl.glDepthMask(gl.GL_TRUE) - - def renderFill(self, posAttrib): - self.prepareFillMask(posAttrib) - - gl.glVertexAttribPointer(posAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, self.bboxVertices) - gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self.bboxVertices)) - - gl.glDisable(gl.GL_STENCIL_TEST) - - def renderStroke(self, posAttrib): - gl.glEnableVertexAttribArray(posAttrib) - gl.glVertexAttribPointer(posAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, self.vertices) - gl.glLineWidth(1) - drawMode = gl.GL_LINE_LOOP if self.strokeClosed else gl.GL_LINE_STRIP - gl.glDrawArrays(drawMode, 0, len(self.vertices)) - - def render(self, posAttrib, colorUnif, hatchStepUnif): - assert self.fill in ['hatch', 'solid', None] - if self.fill is not None: - gl.glUniform4f(colorUnif, *self.fillColor) - step = self._HATCH_STEP if self.fill == 'hatch' else self._NO_HATCH - gl.glUniform1i(hatchStepUnif, step) - self.renderFill(posAttrib) - - if self.stroke: - gl.glUniform4f(colorUnif, *self.strokeColor) - gl.glUniform1i(hatchStepUnif, self._NO_HATCH) - self.renderStroke(posAttrib) - - -# matrix ###################################################################### - -def mat4Ortho(left, right, bottom, top, near, far): - """Orthographic projection matrix (row-major)""" - return numpy.array(( - (2./(right - left), 0., 0., -(right+left)/float(right-left)), - (0., 2./(top - bottom), 0., -(top+bottom)/float(top-bottom)), - (0., 0., -2./(far-near), -(far+near)/float(far-near)), - (0., 0., 0., 1.)), dtype=numpy.float64) - - -def mat4Translate(x=0., y=0., z=0.): - """Translation matrix (row-major)""" - return numpy.array(( - (1., 0., 0., x), - (0., 1., 0., y), - (0., 0., 1., z), - (0., 0., 0., 1.)), dtype=numpy.float64) - - -def mat4Scale(sx=1., sy=1., sz=1.): - """Scale matrix (row-major)""" - return numpy.array(( - (sx, 0., 0., 0.), - (0., sy, 0., 0.), - (0., 0., sz, 0.), - (0., 0., 0., 1.)), dtype=numpy.float64) - - -def mat4Identity(): - """Identity matrix""" - return numpy.array(( - (1., 0., 0., 0.), - (0., 1., 0., 0.), - (0., 0., 1., 0.), - (0., 0., 0., 1.)), dtype=numpy.float64) diff --git a/silx/gui/plot/backends/glutils/GLText.py b/silx/gui/plot/backends/glutils/GLText.py deleted file mode 100644 index 3d262bc..0000000 --- a/silx/gui/plot/backends/glutils/GLText.py +++ /dev/null @@ -1,270 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -""" -This module provides minimalistic text support for OpenGL. -It provides Latin-1 (ISO8859-1) characters for one monospace font at one size. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -from collections import OrderedDict -import numpy - -from ...._glutils import font, gl, getGLContext, Program, Texture -from .GLSupport import mat4Translate - - -# TODO: Font should be configurable by the main program: using mpl.rcParams? - - -class _Cache(object): - """LRU (Least Recent Used) cache. - - :param int maxsize: Maximum number of (key, value) pairs in the cache - :param callable callback: - Called when a (key, value) pair is removed from the cache. - It must take 2 arguments: key and value. - """ - - def __init__(self, maxsize=128, callback=None): - self._maxsize = int(maxsize) - self._callback = callback - self._cache = OrderedDict() - - def __contains__(self, item): - return item in self._cache - - def __getitem__(self, key): - if key in self._cache: - # Remove/add key from ordered dict to store last access info - value = self._cache.pop(key) - self._cache[key] = value - return value - else: - raise KeyError - - def __setitem__(self, key, value): - """Add a key, value pair to the cache. - - :param key: The key to set - :param value: The corresponding value - """ - if key not in self._cache and len(self._cache) >= self._maxsize: - removedKey, removedValue = self._cache.popitem(last=False) - if self._callback is not None: - self._callback(removedKey, removedValue) - self._cache[key] = value - - -# Text2D ###################################################################### - -LEFT, CENTER, RIGHT = 'left', 'center', 'right' -TOP, BASELINE, BOTTOM = 'top', 'baseline', 'bottom' -ROTATE_90, ROTATE_180, ROTATE_270 = 90, 180, 270 - - -class Text2D(object): - - _SHADERS = { - 'vertex': """ - #version 120 - - attribute vec2 position; - attribute vec2 texCoords; - uniform mat4 matrix; - - varying vec2 vCoords; - - void main(void) { - gl_Position = matrix * vec4(position, 0.0, 1.0); - vCoords = texCoords; - } - """, - 'fragment': """ - #version 120 - - uniform sampler2D texText; - uniform vec4 color; - uniform vec4 bgColor; - - varying vec2 vCoords; - - void main(void) { - gl_FragColor = mix(bgColor, color, texture2D(texText, vCoords).r); - } - """ - } - - _TEX_COORDS = numpy.array(((0., 0.), (1., 0.), (0., 1.), (1., 1.)), - dtype=numpy.float32).ravel() - - _program = Program(_SHADERS['vertex'], - _SHADERS['fragment'], - attrib0='position') - - # Discard texture objects when removed from the cache - _textures = _Cache(callback=lambda key, value: value[0].discard()) - """Cache already created textures""" - - _sizes = _Cache() - """Cache already computed sizes""" - - def __init__(self, text, x=0, y=0, - color=(0., 0., 0., 1.), - bgColor=None, - align=LEFT, valign=BASELINE, - rotate=0): - self._vertices = None - self._text = text - self.x = x - self.y = y - self.color = color - self.bgColor = bgColor - - if align not in (LEFT, CENTER, RIGHT): - raise ValueError( - "Horizontal alignment not supported: {0}".format(align)) - self._align = align - - if valign not in (TOP, CENTER, BASELINE, BOTTOM): - raise ValueError( - "Vertical alignment not supported: {0}".format(valign)) - self._valign = valign - - self._rotate = numpy.radians(rotate) - - def _getTexture(self, text): - key = getGLContext(), text - - if key not in self._textures: - image, offset = font.rasterText(text, - font.getDefaultFontFamily()) - if text not in self._sizes: - self._sizes[text] = image.shape[1], image.shape[0] - - self._textures[key] = ( - Texture(gl.GL_RED, - data=image, - minFilter=gl.GL_NEAREST, - magFilter=gl.GL_NEAREST, - wrap=(gl.GL_CLAMP_TO_EDGE, - gl.GL_CLAMP_TO_EDGE)), - offset) - - return self._textures[key] - - @property - def text(self): - return self._text - - @property - def size(self): - if self.text not in self._sizes: - image, offset = font.rasterText(self.text, - font.getDefaultFontFamily()) - self._sizes[self.text] = image.shape[1], image.shape[0] - return self._sizes[self.text] - - def getVertices(self, offset, shape): - height, width = shape - - if self._align == LEFT: - xOrig = 0 - elif self._align == RIGHT: - xOrig = - width - else: # CENTER - xOrig = - width // 2 - - if self._valign == BASELINE: - yOrig = - offset - elif self._valign == TOP: - yOrig = 0 - elif self._valign == BOTTOM: - yOrig = - height - else: # CENTER - yOrig = - height // 2 - - vertices = numpy.array(( - (xOrig, yOrig), - (xOrig + width, yOrig), - (xOrig, yOrig + height), - (xOrig + width, yOrig + height)), dtype=numpy.float32) - - cos, sin = numpy.cos(self._rotate), numpy.sin(self._rotate) - vertices = numpy.ascontiguousarray(numpy.transpose(numpy.array(( - cos * vertices[:, 0] - sin * vertices[:, 1], - sin * vertices[:, 0] + cos * vertices[:, 1]), - dtype=numpy.float32))) - - return vertices - - def render(self, matrix): - if not self.text: - return - - prog = self._program - prog.use() - - texUnit = 0 - texture, offset = self._getTexture(self.text) - - gl.glUniform1i(prog.uniforms['texText'], texUnit) - - mat = numpy.dot(matrix, mat4Translate(int(self.x), int(self.y))) - gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, - mat.astype(numpy.float32)) - - gl.glUniform4f(prog.uniforms['color'], *self.color) - if self.bgColor is not None: - bgColor = self.bgColor - else: - bgColor = self.color[0], self.color[1], self.color[2], 0. - gl.glUniform4f(prog.uniforms['bgColor'], *bgColor) - - vertices = self.getVertices(offset, texture.shape) - - posAttrib = prog.attributes['position'] - gl.glEnableVertexAttribArray(posAttrib) - gl.glVertexAttribPointer(posAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, - vertices) - - texAttrib = prog.attributes['texCoords'] - gl.glEnableVertexAttribArray(texAttrib) - gl.glVertexAttribPointer(texAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, - self._TEX_COORDS) - - with texture: - gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4) diff --git a/silx/gui/plot/backends/glutils/GLTexture.py b/silx/gui/plot/backends/glutils/GLTexture.py deleted file mode 100644 index 25dd9f1..0000000 --- a/silx/gui/plot/backends/glutils/GLTexture.py +++ /dev/null @@ -1,239 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2017 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -"""This module provides classes wrapping OpenGL texture.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -from ctypes import c_void_p -import logging - -import numpy - -from ...._glutils import gl, Texture, numpyToGLType - - -_logger = logging.getLogger(__name__) - - -def _checkTexture2D(internalFormat, shape, - format_=None, type_=gl.GL_FLOAT, border=0): - """Check if texture size with provided parameters is supported - - :rtype: bool - """ - height, width = shape - gl.glTexImage2D(gl.GL_PROXY_TEXTURE_2D, 0, internalFormat, - width, height, border, - format_ or internalFormat, - type_, c_void_p(0)) - width = gl.glGetTexLevelParameteriv( - gl.GL_PROXY_TEXTURE_2D, 0, gl.GL_TEXTURE_WIDTH) - return bool(width) - - -MIN_TEXTURE_SIZE = 64 - - -def _getMaxSquareTexture2DSize(internalFormat=gl.GL_RGBA, - format_=None, - type_=gl.GL_FLOAT, - border=0): - """Returns a supported size for a corresponding square texture - - :returns: GL_MAX_TEXTURE_SIZE or a smaller supported size (not optimal) - :rtype: int - """ - # Is this useful? - maxTexSize = gl.glGetIntegerv(gl.GL_MAX_TEXTURE_SIZE) - while maxTexSize > MIN_TEXTURE_SIZE and \ - not _checkTexture2D(internalFormat, (maxTexSize, maxTexSize), - format_, type_, border): - maxTexSize //= 2 - return max(MIN_TEXTURE_SIZE, maxTexSize) - - -class Image(object): - """Image of any size eventually using multiple textures or larger texture - """ - - _WRAP = (gl.GL_CLAMP_TO_EDGE, gl.GL_CLAMP_TO_EDGE) - _MIN_FILTER = gl.GL_NEAREST - _MAG_FILTER = gl.GL_NEAREST - - def __init__(self, internalFormat, data, format_=None, texUnit=0): - self.internalFormat = internalFormat - self.height, self.width = data.shape[0:2] - type_ = numpyToGLType(data.dtype) - - if _checkTexture2D(internalFormat, data.shape[0:2], format_, type_): - texture = Texture(internalFormat, - data, - format_, - texUnit=texUnit, - minFilter=self._MIN_FILTER, - magFilter=self._MAG_FILTER, - wrap=self._WRAP) - vertices = numpy.array(( - (0., 0., 0., 0.), - (self.width, 0., 1., 0.), - (0., self.height, 0., 1.), - (self.width, self.height, 1., 1.)), dtype=numpy.float32) - self.tiles = ((texture, vertices, - {'xOrigData': 0, 'yOrigData': 0, - 'wData': self.width, 'hData': self.height}),) - - else: - # Handle dimension too large: make tiles - maxTexSize = _getMaxSquareTexture2DSize(internalFormat, - format_, type_) - - nCols = (self.width+maxTexSize-1) // maxTexSize - colWidths = [self.width // nCols] * nCols - colWidths[-1] += self.width % nCols - - nRows = (self.height+maxTexSize-1) // maxTexSize - rowHeights = [self.height//nRows] * nRows - rowHeights[-1] += self.height % nRows - - tiles = [] - yOrig = 0 - for hData in rowHeights: - xOrig = 0 - for wData in colWidths: - if (hData < MIN_TEXTURE_SIZE or wData < MIN_TEXTURE_SIZE) \ - and not _checkTexture2D(internalFormat, - (hData, wData), - format_, - type_): - # Ensure texture size is at least MIN_TEXTURE_SIZE - tH = max(hData, MIN_TEXTURE_SIZE) - tW = max(wData, MIN_TEXTURE_SIZE) - - uMax, vMax = float(wData)/tW, float(hData)/tH - - # TODO issue with type_ and alignment - texture = Texture(internalFormat, - data=None, - format_=format_, - shape=(tH, tW), - texUnit=texUnit, - minFilter=self._MIN_FILTER, - magFilter=self._MAG_FILTER, - wrap=self._WRAP) - # TODO handle unpack - texture.update(format_, - data[yOrig:yOrig+hData, - xOrig:xOrig+wData]) - # texture.update(format_, type_, data, - # width=wData, height=hData, - # unpackRowLength=width, - # unpackSkipPixels=xOrig, - # unpackSkipRows=yOrig) - else: - uMax, vMax = 1, 1 - # TODO issue with type_ and unpacking tiles - # TODO idea to handle unpack: use array strides - # As it is now, it will make a copy - texture = Texture(internalFormat, - data[yOrig:yOrig+hData, - xOrig:xOrig+wData], - format_, - shape=(hData, wData), - texUnit=texUnit, - minFilter=self._MIN_FILTER, - magFilter=self._MAG_FILTER, - wrap=self._WRAP) - # TODO - # unpackRowLength=width, - # unpackSkipPixels=xOrig, - # unpackSkipRows=yOrig) - vertices = numpy.array(( - (xOrig, yOrig, 0., 0.), - (xOrig + wData, yOrig, uMax, 0.), - (xOrig, yOrig + hData, 0., vMax), - (xOrig + wData, yOrig + hData, uMax, vMax)), - dtype=numpy.float32) - tiles.append((texture, vertices, - {'xOrigData': xOrig, 'yOrigData': yOrig, - 'wData': wData, 'hData': hData})) - xOrig += wData - yOrig += hData - self.tiles = tuple(tiles) - - def discard(self): - for texture, vertices, _ in self.tiles: - texture.discard() - del self.tiles - - def updateAll(self, format_, data, texUnit=0): - if not hasattr(self, 'tiles'): - raise RuntimeError("No texture, discard has already been called") - - assert data.shape[:2] == (self.height, self.width) - if len(self.tiles) == 1: - self.tiles[0][0].update(format_, data, texUnit=texUnit) - else: - for texture, _, info in self.tiles: - yOrig, xOrig = info['yOrigData'], info['xOrigData'] - height, width = info['hData'], info['wData'] - texture.update(format_, - data[yOrig:yOrig+height, xOrig:xOrig+width], - texUnit=texUnit) - # TODO check - # width=info['wData'], height=info['hData'], - # texUnit=texUnit, unpackAlign=unpackAlign, - # unpackRowLength=self.width, - # unpackSkipPixels=info['xOrigData'], - # unpackSkipRows=info['yOrigData']) - - def render(self, posAttrib, texAttrib, texUnit=0): - try: - tiles = self.tiles - except AttributeError: - raise RuntimeError("No texture, discard has already been called") - - for texture, vertices, _ in tiles: - texture.bind(texUnit) - - stride = vertices.shape[-1] * vertices.itemsize - gl.glEnableVertexAttribArray(posAttrib) - gl.glVertexAttribPointer(posAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - stride, vertices) - - texCoordsPtr = c_void_p(vertices.ctypes.data + - 2 * vertices.itemsize) - gl.glEnableVertexAttribArray(texAttrib) - gl.glVertexAttribPointer(texAttrib, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - stride, texCoordsPtr) - gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices)) diff --git a/silx/gui/plot/backends/glutils/PlotImageFile.py b/silx/gui/plot/backends/glutils/PlotImageFile.py deleted file mode 100644 index 83c7ae0..0000000 --- a/silx/gui/plot/backends/glutils/PlotImageFile.py +++ /dev/null @@ -1,153 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2017 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -"""Function to save an image to a file.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -import base64 -import struct -import sys -import zlib - - -# Image writer ################################################################ - -def convertRGBDataToPNG(data): - """Convert a RGB bitmap to PNG. - - It only supports RGB bitmap with one byte per channel stored as a 3D array. - See `Definitive Guide <http://www.libpng.org/pub/png/book/>`_ and - `Specification <http://www.libpng.org/pub/png/spec/1.2/>`_ for details. - - :param data: A 3D array (h, w, rgb) storing an RGB image - :type data: numpy.ndarray of unsigned bytes - :returns: The PNG encoded data - :rtype: bytes - """ - height, width = data.shape[0], data.shape[1] - depth = 8 # 8 bit per channel - colorType = 2 # 'truecolor' = RGB - interlace = 0 # No - - IHDRdata = struct.pack(">ccccIIBBBBB", b'I', b'H', b'D', b'R', - width, height, depth, colorType, - 0, 0, interlace) - - # Add filter 'None' before each scanline - preparedData = b'\x00' + b'\x00'.join(line.tostring() for line in data) - compressedData = zlib.compress(preparedData, 8) - - IDATdata = struct.pack("cccc", b'I', b'D', b'A', b'T') - IDATdata += compressedData - - return b''.join([ - b'\x89PNG\r\n\x1a\n', # PNG signature - # IHDR chunk: Image Header - struct.pack(">I", 13), # length - IHDRdata, - struct.pack(">I", zlib.crc32(IHDRdata) & 0xffffffff), # CRC - # IDAT chunk: Payload - struct.pack(">I", len(compressedData)), - IDATdata, - struct.pack(">I", zlib.crc32(IDATdata) & 0xffffffff), # CRC - b'\x00\x00\x00\x00IEND\xaeB`\x82' # IEND chunk: footer - ]) - - -def saveImageToFile(data, fileNameOrObj, fileFormat): - """Save a RGB image to a file. - - :param data: A 3D array (h, w, 3) storing an RGB image. - :type data: numpy.ndarray with of unsigned bytes. - :param fileNameOrObj: Filename or object to use to write the image. - :type fileNameOrObj: A str or a 'file-like' object with a 'write' method. - :param str fileFormat: The type of the file in: 'png', 'ppm', 'svg', 'tiff'. - """ - assert len(data.shape) == 3 - assert data.shape[2] == 3 - assert fileFormat in ('png', 'ppm', 'svg', 'tiff') - - if not hasattr(fileNameOrObj, 'write'): - if sys.version_info < (3, ): - fileObj = open(fileNameOrObj, "wb") - else: - if fileFormat in ('png', 'ppm', 'tiff'): - # Open in binary mode - fileObj = open(fileNameOrObj, 'wb') - else: - fileObj = open(fileNameOrObj, 'w', newline='') - else: # Use as a file-like object - fileObj = fileNameOrObj - - if fileFormat == 'svg': - height, width = data.shape[:2] - base64Data = base64.b64encode(convertRGBDataToPNG(data)) - - fileObj.write( - '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n') - fileObj.write('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n') - fileObj.write( - ' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n') - fileObj.write('<svg xmlns:xlink="http://www.w3.org/1999/xlink"\n') - fileObj.write(' xmlns="http://www.w3.org/2000/svg"\n') - fileObj.write(' version="1.1"\n') - fileObj.write(' width="%d"\n' % width) - fileObj.write(' height="%d">\n' % height) - fileObj.write(' <image xlink:href="data:image/png;base64,') - fileObj.write(base64Data.decode('ascii')) - fileObj.write('"\n') - fileObj.write(' x="0"\n') - fileObj.write(' y="0"\n') - fileObj.write(' width="%d"\n' % width) - fileObj.write(' height="%d"\n' % height) - fileObj.write(' id="image" />\n') - fileObj.write('</svg>') - - elif fileFormat == 'ppm': - height, width = data.shape[:2] - - fileObj.write(b'P6\n') - fileObj.write(b'%d %d\n' % (width, height)) - fileObj.write(b'255\n') - fileObj.write(data.tostring()) - - elif fileFormat == 'png': - fileObj.write(convertRGBDataToPNG(data)) - - elif fileFormat == 'tiff': - if fileObj == fileNameOrObj: - raise NotImplementedError( - 'Save TIFF to a file-like object not implemented') - - from silx.third_party.TiffIO import TiffIO - - tif = TiffIO(fileNameOrObj, mode='wb+') - tif.writeImage(data, info={'Title': 'OpenGL Plot Snapshot'}) - - if fileObj != fileNameOrObj: - fileObj.close() diff --git a/silx/gui/plot/backends/glutils/__init__.py b/silx/gui/plot/backends/glutils/__init__.py deleted file mode 100644 index 771de39..0000000 --- a/silx/gui/plot/backends/glutils/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2017 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################*/ -"""This module provides convenient classes for the OpenGL rendering backend. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "03/04/2017" - - -import logging - - -_logger = logging.getLogger(__name__) - - -from .GLPlotCurve import * # noqa -from .GLPlotFrame import * # noqa -from .GLPlotImage import * # noqa -from .GLSupport import * # noqa -from .GLText import * # noqa -from .GLTexture import * # noqa |