1#!/usr/bin/env python
2# -*- mode: python; coding: utf-8; -*-
3# ---------------------------------------------------------------------------##
4#
5#  Copyright (C) 1998-2003 Markus Franz Xaver Johannes Oberhumer
6#  Copyright (C) 2003 Mt. Hood Playing Card Co.
7#  Copyright (C) 2005-2009 Skomoroh
8#
9#  This program is free software: you can redistribute it and/or modify
10#  it under the terms of the GNU General Public License as published by
11#  the Free Software Foundation, either version 3 of the License, or
12#  (at your option) any later version.
13#
14#  This program is distributed in the hope that it will be useful,
15#  but WITHOUT ANY WARRANTY; without even the implied warranty of
16#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17#  GNU General Public License for more details.
18#
19#  You should have received a copy of the GNU General Public License
20#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
21#
22# ---------------------------------------------------------------------------##
23
24
25import os
26
27from pysollib.mfxutil import Image, ImageTk, USE_PIL, print_err
28from pysollib.pysoltk import copyImage, createBottom, createImage, loadImage
29from pysollib.pysoltk import shadowImage
30from pysollib.resource import CSI
31from pysollib.settings import TOOLKIT
32
33# ************************************************************************
34# * Images
35# ************************************************************************
36
37
38class ImagesCardback:
39    def __init__(self, index, name, image, menu_image=None):
40        if menu_image is None:
41            menu_image = image
42        self.index = index
43        self.name = name
44        self.image = image
45        self.menu_image = menu_image
46
47
48class Images:
49    def __init__(self, dataloader, cs, r=1):
50        self.d = dataloader
51        self.cs = cs
52        self.reduced = r
53        self._xfactor = 1.0
54        self._yfactor = 1.0
55        if cs is None:
56            return
57        self._setSize()
58        self._card = []
59        self._back = []
60        # bottom of stack (link to _bottom_negative/_bottom_positive)
61        self._bottom = []
62        self._bottom_negative = []      # negative bottom of stack (white)
63        self._bottom_positive = []      # positive bottom of stack (black)
64        self._blank_bottom = None       # blank (transparent) bottom of stack
65        self._letter = []               # images of letter
66        self._letter_negative = []
67        self._letter_positive = []
68        # vertical shadow of card (used when we drag a card)
69        self._shadow = []
70        self._xshadow = []              # horizontal shadow of card
71        self._pil_shadow = {}           # key: (width, height)
72        self._highlight = []            # highlight of card (tip)
73        self._highlight_index = 0       #
74        self._highlighted_images = {}   # key: (suit, rank)
75
76    def destruct(self):
77        pass
78
79    def __loadCard(self, filename, check_w=1, check_h=1):
80        # print '__loadCard:', filename
81        f = os.path.join(self.cs.dir, filename)
82        if not os.path.exists(f):
83            print_err('card image path %s does not exist' % f)
84            return None
85        try:
86            img = loadImage(file=f)
87        except Exception:
88            return None
89
90        if TOOLKIT == 'kivy':
91            w = img.texture.size[0]
92            h = img.texture.size[1]
93        else:
94            w, h = img.width(), img.height()
95
96        if self.CARDW < 0:
97            self.CARDW, self.CARDH = w, h
98        else:
99            if ((check_w and w != self.CARDW) or
100                    (check_h and h != self.CARDH)):
101                raise ValueError("Invalid size %dx%d of image %s" % (w, h, f))
102        return img
103
104    def __loadBottom(self, filename, check_w=1, check_h=1, color='white'):
105        cs_type = CSI.TYPE_ID[self.cs.type]
106        imagedir = None
107        d = os.path.join('images', 'cards', 'bottoms')
108        try:
109            imagedir = self.d.findDir(cs_type, d)
110        except Exception:
111            pass
112        if (not USE_PIL and TOOLKIT != 'kivy') or imagedir is None:
113            # load image
114            img = self.__loadCard(filename+self.cs.ext, check_w, check_h)
115            if USE_PIL and img is not None:
116                # we have no bottom images
117                # (data/images/cards/bottoms/<cs_type>)
118                img = img.resize(self._xfactor, self._yfactor)
119            return img
120        # create image
121        d = os.path.join('images', 'cards', 'bottoms', cs_type)
122        try:
123            fn = self.d.findImage(filename, d)
124        except Exception:
125            fn = None
126        img = createBottom(self._card[0], color, fn)
127        return img
128
129    def __addBack(self, im1, name):
130        r = max(self.CARDW / 40.0, self.CARDH / 60.0)
131        r = max(2, int(round(r)))
132        im2 = im1.subsample(r)
133        self._back.append(ImagesCardback(len(self._back), name, im1, im2))
134
135    def _createMissingImages(self):
136        cw, ch = self.getSize()
137        # back
138        if not self._back:
139            im = createImage(cw, ch, fill="#a0a0a0", outline="#000000")
140            name = ""
141            self.__addBack(im, name)
142            self.cs.backnames = tuple(self.cs.backnames) + (name,)
143        # bottoms / letters
144        bottom = None
145        neg_bottom = None
146        while len(self._bottom_positive) < max(7, self.cs.nbottoms):
147            if bottom is None:
148                bottom = createImage(cw, ch, fill=None, outline="#000000")
149            self._bottom_positive.append(bottom)
150        while len(self._bottom_negative) < max(7, self.cs.nbottoms):
151            if neg_bottom is None:
152                neg_bottom = createImage(cw, ch, fill=None, outline="#ffffff")
153            self._bottom_negative.append(neg_bottom)
154        while len(self._letter_positive) < 4:
155            if bottom is None:
156                bottom = createImage(cw, ch, fill=None, outline="#000000")
157            self._letter_positive.append(bottom)
158        while len(self._letter_negative) < 4:
159            if neg_bottom is None:
160                neg_bottom = createImage(cw, ch, fill=None, outline="#ffffff")
161            self._letter_negative.append(neg_bottom)
162        self._blank_bottom = createImage(cw, ch, fill=None, outline=None)
163
164    def load(self, app, progress=None):
165        ext = self.cs.ext[1:]
166        pstep = 0
167        if progress:
168            pstep = self.cs.ncards + len(self.cs.backnames) + \
169                    self.cs.nbottoms + self.cs.nletters
170            pstep += self.cs.nshadows + 1  # shadows & shade
171            pstep = max(0, (80.0 - progress.percent) / pstep)
172        # load face cards
173        for n in self.cs.getFaceCardNames():
174            self._card.append(self.__loadCard(n + self.cs.ext))
175            self._card[-1].filename = n
176            if progress:
177                progress.update(step=pstep)
178        assert len(self._card) == self.cs.ncards
179        # load backgrounds
180        for name in self.cs.backnames:
181            if name:
182                im = self.__loadCard(name)
183                if im:
184                    self.__addBack(im, name)
185                else:
186                    print_err('in {cs_dir}/config.txt: card back "{fname}" '
187                              'does not exist'.format(
188                                  cs_dir=self.cs.dir, fname=name))
189        if progress:
190            progress.update(step=1)
191        # load bottoms
192        for i in range(self.cs.nbottoms):
193            name = "bottom%02d" % (i + 1)
194            bottom = self.__loadBottom(name, color='black')
195            if bottom is not None:
196                self._bottom_positive.append(bottom)
197            if progress:
198                progress.update(step=pstep)
199            # load negative bottoms
200            name = "bottom%02d-n" % (i + 1)
201            bottom = self.__loadBottom(name, color='white')
202            if bottom is not None:
203                self._bottom_negative.append(bottom)
204            if progress:
205                progress.update(step=pstep)
206        # load letters
207        for rank in range(self.cs.nletters):
208            name = "l%02d" % (rank + 1)
209            self._letter_positive.append(
210                self.__loadBottom(name, color='black'))
211            if progress:
212                progress.update(step=pstep)
213            # load negative letters
214            name = "l%02d-n" % (rank + 1)
215            self._letter_negative.append(
216                self.__loadBottom(name, color='white'))
217            if progress:
218                progress.update(step=pstep)
219        # shadow
220        if not USE_PIL:
221            for i in range(self.cs.nshadows):
222                name = "shadow%02d.%s" % (i, ext)
223                im = self.__loadCard(name, check_w=0, check_h=0)
224                self._shadow.append(im)
225                if i > 0:  # skip 0
226                    name = "xshadow%02d.%s" % (i, ext)
227                    im = self.__loadCard(name, check_w=0, check_h=0)
228                    self._xshadow.append(im)
229                if progress:
230                    progress.update(step=pstep)
231        # shade
232        if USE_PIL:
233            self._highlight.append(
234                self._getHighlight(self._card[0], None, '#3896f8'))
235        else:
236            self._highlight.append(self.__loadCard("shade." + ext))
237        if progress:
238            progress.update(step=pstep)
239        # create missing
240        self._createMissingImages()
241        #
242        self._bottom = self._bottom_positive
243        self._letter = self._letter_positive
244        #
245        return 1
246
247    def getFace(self, deck, suit, rank):
248        index = suit * len(self.cs.ranks) + rank
249        # print "getFace:", suit, rank, index
250        return self._card[index % self.cs.ncards]
251
252    def getBack(self, update=False):
253        if update:
254            self._shadow_back = None
255        index = self.cs.backindex % len(self._back)
256        return self._back[index].image
257
258    def getTalonBottom(self):
259        return self._bottom[0]
260
261    def getReserveBottom(self):
262        return self._bottom[0]
263
264    def getBlankBottom(self):
265        if TOOLKIT == 'kivy':
266            return self._bottom[0]
267        return self._blank_bottom
268
269    def getSuitBottom(self, suit=-1):
270        assert isinstance(suit, int)
271        if suit == -1:
272            return self._bottom[1]   # any suit
273        i = 3 + suit
274        if i >= len(self._bottom):
275            # Trump (for Tarock type games)
276            return self._bottom[1]
277        return self._bottom[i]
278
279    def getBraidBottom(self):
280        return self._bottom[2]
281
282    def getLetter(self, rank):
283        assert 0 <= rank <= 3
284        if rank >= len(self._letter):
285            return self._bottom[0]
286        return self._letter[rank]
287
288    def getShadow(self, ncards):
289        if ncards >= 0:
290            if ncards >= len(self._shadow):
291                # ncards = len(self._shadow) - 1
292                return None
293            return self._shadow[ncards]
294        else:
295            ncards = abs(ncards)-2
296            if ncards >= len(self._xshadow):
297                return None
298            return self._xshadow[ncards]
299
300    def getShadowPIL(self, stack, cards):
301        x0, y0 = stack.getPositionFor(cards[0])
302        x1, y1 = stack.getPositionFor(cards[-1])
303        x0, x1 = min(x1, x0), max(x1, x0)
304        y0, y1 = min(y1, y0), max(y1, y0)
305        cw, ch = self.getSize()
306        x1 += cw
307        y1 += ch
308        w, h = x1-x0, y1-y0
309        if (w, h) in self._pil_shadow:
310            return self._pil_shadow[(w, h)]
311        # create mask
312        mask = Image.new('RGBA', (w, h))
313        for c in cards:
314            x, y = stack.getPositionFor(c)
315            x, y = x-x0, y-y0
316            im = c._active_image._pil_image
317            mask.paste(im, (x, y), im)
318        # create shadow
319        sh_color = (0x00, 0x00, 0x00, 0x50)
320        shadow = Image.new('RGBA', (w, h))
321        shadow.paste(sh_color, (0, 0, w, h), mask)
322        sx, sy = self.SHADOW_XOFFSET, self.SHADOW_YOFFSET
323        mask = mask.crop((sx, sy, w, h))
324        tmp = Image.new('RGBA', (w-sx, h-sy))
325        shadow.paste(tmp, (0, 0), mask)
326        shadow = ImageTk.PhotoImage(shadow)
327        self._pil_shadow[(w, h)] = shadow
328        return shadow
329
330    def getShade(self):
331        # highlight
332        return self._highlight[self._highlight_index]
333
334    def _getHighlight(self, image, card, color='#3896f8', factor=0.3):
335        if USE_PIL:
336            # use semitransparent image; one for each color (PIL >= 1.1.7)
337            if color in self._highlighted_images:
338                shade = self._highlighted_images[color]
339            else:
340                shade = shadowImage(image, color, factor)
341                self._highlighted_images[color] = shade
342        else:
343            # use alpha blending (PIL <= 1.1.6)
344            if card in self._highlighted_images:
345                shade = self._highlighted_images[card]
346            else:
347                shade = shadowImage(image, color, factor)
348                self._highlighted_images[card] = shade
349        if not shade:
350            # we have not PIL
351            return self.getShade()
352        return shade
353
354    def getHighlightedCard(self, deck, suit, rank, color=None):
355        image = self.getFace(deck, suit, rank)
356        if color:
357            return self._getHighlight(image, (suit, rank, color), color)
358        return self._getHighlight(image, (suit, rank))
359
360    def getHighlightedBack(self):
361        image = self.getBack()
362        return self._getHighlight(image, 'back')
363
364    def getCardbacks(self):
365        return self._back
366
367    def setNegative(self, flag=0):
368        if flag:
369            self._bottom = self._bottom_negative
370            self._letter = self._letter_negative
371        else:
372            self._bottom = self._bottom_positive
373            self._letter = self._letter_positive
374
375    def setOffsets(self):
376        cs = self.cs
377        if cs is None:
378            return
379        r = self.reduced
380        if r > 1:
381            self.CARD_XOFFSET = max(10, cs.CARD_XOFFSET // r)
382            self.CARD_YOFFSET = max(10, cs.CARD_YOFFSET // r)
383        else:
384            self.CARD_XOFFSET = cs.CARD_XOFFSET
385            self.CARD_YOFFSET = cs.CARD_YOFFSET
386        self.SHADOW_XOFFSET = cs.SHADOW_XOFFSET
387        self.SHADOW_YOFFSET = cs.SHADOW_YOFFSET
388        self.CARD_DX, self.CARD_DY = cs.CARD_DX, cs.CARD_DY
389
390    def _setSize(self, xf=1, yf=1):
391        # print 'image._setSize', xf, yf
392        self._xfactor = xf
393        self._yfactor = yf
394        cs = self.cs
395        if cs is None:
396            return
397        r = self.reduced
398        xf = float(xf)/r
399        yf = float(yf)/r
400        # from cardset
401        self.CARDW, self.CARDH = int(cs.CARDW*xf), int(cs.CARDH*yf)
402        self.setOffsets()
403
404    def getSize(self):
405        return (int(self.CARDW * self._xfactor),
406                int(self.CARDH * self._yfactor))
407
408    def getOffsets(self):
409        return (int(self.CARD_XOFFSET * self._xfactor),
410                int(self.CARD_YOFFSET * self._yfactor))
411
412    def getDelta(self):
413        return (int(self.CARD_DX * self._xfactor),
414                int(self.CARD_DY * self._yfactor))
415
416    def resize(self, xf, yf):
417        # print 'Images.resize:', xf, yf, self._card[0].width(), self.CARDW
418        if self._xfactor == xf and self._yfactor == yf:
419            # print 'no resize'
420            return
421        self._xfactor = xf
422        self._yfactor = yf
423        # ???self._setSize(xf, yf)
424        self.setOffsets()
425        # cards
426        cards = []
427        for c in self._card:
428            c = c.resize(xf, yf)
429            cards.append(c)
430        self._card = cards
431        # back
432        for b in self._back:
433            b.image = b.image.resize(xf, yf)
434        # stack bottom image
435        neg = self._bottom is self._bottom_negative
436        self._bottom_negative = []
437        self._bottom_positive = []
438        for i in range(self.cs.nbottoms):
439            name = "bottom%02d" % (i + 1)
440            bottom = self.__loadBottom(name, color='black')
441            if bottom is not None:
442                self._bottom_positive.append(bottom)
443            name = "bottom%02d-n" % (i + 1)
444            bottom = self.__loadBottom(name, color='white')
445            if bottom is not None:
446                self._bottom_negative.append(bottom)
447        # letters
448        self._letter_positive = []
449        self._letter_negative = []
450        for rank in range(self.cs.nletters):
451            name = "l%02d" % (rank + 1)
452            self._letter_positive.append(
453                self.__loadBottom(name, color='black'))
454            name = "l%02d-n" % (rank + 1)
455            self._letter_negative.append(
456                self.__loadBottom(name, color='white'))
457        self._createMissingImages()
458        self.setNegative(neg)
459        #
460        self._highlighted_images = {}
461        self._highlight = []
462        self._highlight.append(
463            self._getHighlight(self._card[0], None, '#3896f8'))
464        self._pil_shadow = {}
465
466    def reset(self):
467        print('Image.reset')
468        self.resize(1, 1)
469
470
471# ************************************************************************
472# *
473# ************************************************************************
474
475class SubsampledImages(Images):
476    def __init__(self, images, r=2):
477        size_cap = 100
478        if images.CARDW // r > size_cap or images.CARDH // r > size_cap:
479            r = max(images.CARDW, images.CARDH) // size_cap
480
481        Images.__init__(self, None, images.cs, r=r)
482        self._card = self._subsample(images._card, r)
483        self._bottom_positive = self._subsample(images._bottom_positive, r)
484        self._letter_positive = self._subsample(images._letter_positive, r)
485        self._bottom_negative = self._subsample(images._bottom_negative, r)
486        self._letter_negative = self._subsample(images._letter_negative, r)
487        self._bottom = self._bottom_positive
488        self._letter = self._letter_positive
489
490        #
491        for _back in images._back:
492            if _back is None:
493                self._back.append(None)
494            else:
495                im = _back.image.subsample(r)
496                self._back.append(
497                    ImagesCardback(len(self._back), _back.name, im, im))
498        #
499        CW, CH = self.CARDW, self.CARDH
500        for im in images._highlight:
501            # self._highlight.append(None)
502            self._highlight.append(copyImage(im, 0, 0, CW, CH))
503
504    def getShadow(self, ncards):
505        return None
506
507    def _subsample(self, images_list, r):
508        s = []
509        for im in images_list:
510            if im is None or r == 1:
511                s.append(im)
512            else:
513                s.append(im.subsample(r))
514        return s
515