Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/python3
- '''Creates a multicolumn screenshot from a series of Facebook screenshots.
- NAME
- stitch
- SYNOPSIS
- stitch [-h] [-d DIR] [-f FMT] [-g GUTTER] [-y HEIGHT] [-m MARGIN] [-r]
- [-t TOLERANCE] [-q] [-v] [-V] number-of-screenshots output-file
- DESCRIPTION
- This script loads a series of Facebook screenshots of a single comment
- thread and generates a large multicolumn screenshot image file. The filenames
- are indicated by means of a C format string. The defult format string is
- "%02d.png" so that the image filenames are 01.png, 02.png, 03.png and so on.
- Author:
- Daniel Neville (Blancmange), creamygoat@gmail.com
- Copyright:
- None
- Licence:
- Public domain
- INDEX
- Imports
- Constants
- Exceptions:
- Error
- ArgError
- FileError
- LimitError
- Vector functions:
- VNeg(A)
- VSum(*VectorArgs)
- VDiff(A, B)
- VManhattan(A)
- tVStrip:
- __init__(Imgs)
- __getitem__(Pos)
- __setitem__(Pos, Colour)
- IndexY(y)
- Segments(Box)
- GetPixel(Pos)
- SetPixel(Pos, Colour)
- SubImage(SourceBox)
- PasteTo(DestImg, Pos, SourceBox)
- Imgs
- Fences
- Count
- Width
- Height
- Pixel scanning functions:
- PixelInBounds(Pos, Bounds)
- FindFirstPixel(Pixels, Bounds, MatchValue, StartPos, DeltaPos)
- FindRunEnd(Pixels, Bounds, StartPos, DeltaPos)
- Vertical stitching functions:
- FindStripBounds(Img)
- ImageVOverlaps(Img1, Img2, Tolerance=0)
- WeightedAverageOverlap(OverlapLists)
- MostLikelyOverlaps(OverlapLists)
- NonOverlappingImages(OverlappingImages, Overlaps)
- VTrimmedVStrip(VStrip)
- Comment detection and layout:
- FindCommentBreak(VStrip, StartPos, RightMargin, BottomMargin)
- FindCommentBreakMetrics(VStrip)
- WrappedCommentRuns(
- CmtExts, CapHeight, Margin, Gutter, TargetHeight, StripWidth)
- Image modification functions:
- SuppressNewCommentsMarker(VStrip)
- NewCapImgs(VStrip, CmtBreakSpan, CapHeight)
- Main:
- Main()
- '''
- #-------------------------------------------------------------------------------
- # Imports
- #-------------------------------------------------------------------------------
- import sys
- import traceback
- import os
- import argparse
- from math import sqrt
- from PIL import Image
- #-------------------------------------------------------------------------------
- # Constants
- #-------------------------------------------------------------------------------
- # Version string
- VersionStr = '1.0.1.0'
- # Detection colours
- PageBGColour = (233, 235, 238)
- HeaderBGColour = (255, 255, 255)
- CommentBGColour = (246, 247, 249)
- NewCommentMarkerColour = (64, 128, 255)
- # Drawing colours
- TopCapColour = (229, 230, 233)
- BottomCapColour = (208, 209, 213)
- DiscontinuityColour = (255, 0, 0)
- # Pixel step vectors
- vLeft = (-1, 0)
- vRight = (1, 0)
- vUp = (0, -1)
- vDown = (0, 1)
- #-------------------------------------------------------------------------------
- # Exceptions
- #-------------------------------------------------------------------------------
- class Error (Exception):
- pass
- class ArgError (Error):
- pass
- class FileError(Error):
- pass
- class LimitError(Error):
- pass
- class ScanningError(Error):
- pass
- #-------------------------------------------------------------------------------
- # Vector functions
- #-------------------------------------------------------------------------------
- def VNeg(A):
- return tuple(-x for x in A)
- def VSum(*VectorArgs):
- if len(VectorArgs) == 1:
- Vectors = VectorArgs[0]
- else:
- Vectors = VectorArgs
- Result = tuple(Vectors[0])
- for i in range(1, len(Vectors)):
- Result = tuple(a + b for a, b in zip(Result, Vectors[i]))
- return Result
- def VDiff(A, B):
- return tuple(x - y for x, y in zip(A, B))
- def VManhattan(A):
- return sum(abs(x) for x in A)
- #-------------------------------------------------------------------------------
- # tVStrip
- #-------------------------------------------------------------------------------
- class tVStrip (object):
- '''Keeps a list of vertically stacked images.
- The pixels in a tVStrip can be accessed with [x, y], just like the pixel
- access object provided by the load() method of images.
- Methods:
- __init__(Imgs)
- __getitem__(Pos)
- __setitem__(Pos, Colour)
- IndexY(y)
- Segments(Box)
- GetPixel(Pos)
- SetPixel(Pos, Colour)
- SubImage(SourceBox)
- PasteTo(DestImg, Pos, SourceBox)
- Fields:
- Imgs
- Fences
- Count
- Width
- Height
- '''
- #-----------------------------------------------------------------------------
- def __init__(self, Imgs):
- '''Create a keeper of vertically stacked images to be alalysed as a unit.
- Imgs is a sequence of images of the same width, in top-to-bottom order.
- '''
- self.Imgs = Imgs
- self.Fences = [0]
- y = 0
- for Img in self.Imgs:
- h = Img.size[1]
- self.Fences.append(y + h)
- y += h
- self._IndexYs = [(0, 0)] * self.Height
- Y = 0
- SubY = 0
- for i, Fence in enumerate(self.Fences[1:]):
- while Y < Fence:
- self._IndexYs[Y] = (i, SubY)
- Y += 1
- SubY += 1
- SubY = 0
- #-----------------------------------------------------------------------------
- def __getitem__(self, Pos):
- '''Return a pixel colour with the form Colour = VStrip[x, y].'''
- if (Pos is tuple or Pos is list) and len(Pos) == 2:
- raise TypeError('An (x, y) tuple is required.')
- if not (0 <= Pos[0] < self.Width and 0 <= Pos[1] < self.Height):
- raise KeyError('Pixel coordinates ' + Pos + ' out of bounds.')
- return self.GetPixel(Pos)
- #-----------------------------------------------------------------------------
- def __setitem__(self, Pos, Colour):
- '''Set a pixel colour with the form VStrip[x, y] = Colour.'''
- if (Pos is tuple or Pos is list) and len(Pos) == 2:
- raise TypeError('An (x, y) tuple is required.')
- if not (0 <= Pos[0] < self.Width and 0 <= Pos[1] < self.Height):
- raise KeyError('Pixel coordinates ' + Pos + ' out of bounds.')
- self.SetPixel(Pos, Colour)
- #-----------------------------------------------------------------------------
- @property
- def Count(self):
- '''Return the number of images loaded.'''
- return len(self.Imgs)
- @property
- def Width(self):
- '''Return the width of the image strip.'''
- return self.Imgs[0].size[0] if len(self.Imgs) > 0 else None
- @property
- def Height(self):
- '''Return the height of the image strip.'''
- return self.Fences[-1]
- #-----------------------------------------------------------------------------
- def IndexY(self, y):
- '''Return the (zero-based) image index and its row index given y.'''
- return self._IndexYs[y]
- #-----------------------------------------------------------------------------
- def Segments(self, Box):
- '''Return image indices and image bounds spanned by Box.
- Box is in the form (Left, Top, Right, Bottom). Box must already be
- correctly clipped horizontally.
- The result is a list, possibly empty, of image-bound pairs of the form
- (Index, (Left, Top, Right, Bottom)).
- '''
- Result = []
- BoxWidth = Box[2] - Box[0]
- BoxHeight = Box[3] - Box[1]
- if BoxWidth > 0 and BoxHeight > 0:
- Ix, SubY0 = self._IndexYs[Box[1]]
- UnclippedSubY1 = SubY0 + BoxHeight
- while UnclippedSubY1 > SubY0:
- ImgH = self.Fences[Ix + 1] - self.Fences[Ix]
- SubY1 = min(ImgH, UnclippedSubY1)
- Result.append((Ix, (Box[0], SubY0, Box[2], SubY1)))
- Ix += 1
- SubY0 = 0
- UnclippedSubY1 -= ImgH
- return Result
- #-----------------------------------------------------------------------------
- def GetPixel(self, Pos):
- '''Get a single pixel from the strip image.
- No bounds checking is performed.
- '''
- n, y = self._IndexYs[Pos[1]]
- return self.Imgs[n].getpixel((Pos[0], y))
- #-----------------------------------------------------------------------------
- def SetPixel(self, Pos, Colour):
- '''Set the colour of a single pixel from the strip image.
- No bounds checking is performed.
- '''
- n, y = self._IndexYs[Pos[1]]
- self.Imgs[n].putpixel((Pos[0], y), Colour)
- #-----------------------------------------------------------------------------
- def SubImage(self, Box):
- '''Returns a image copied from a portion of the strip image.
- Though Box may straddle component images, it must already be clipped
- to the boundaries of the strip image.
- '''
- w = Box[2] - Box[0]
- h = Box[3] - Box[1]
- Result = Image.new(self.Imgs[0].mode, (w, h))
- self.PasteTo(Result, (0, 0), Box)
- return Result
- #-----------------------------------------------------------------------------
- def PasteTo(self, DestImg, Pos, SourceBox):
- '''Pastes a portion of the strip image to a portion of a regular image.
- Though SourceBox may straddle component images, it must already be clipped
- to the boundaries of the strip image.
- '''
- S = self.Segments(SourceBox)
- x, y = Pos
- for (Ix, Box) in S:
- DestImg.paste(self.Imgs[Ix].crop(Box), (x, y))
- y += Box[3] - Box[1]
- #-----------------------------------------------------------------------------
- #-------------------------------------------------------------------------------
- # Pixel scanning functions
- #-------------------------------------------------------------------------------
- def PixelInBounds(Pos, Bounds):
- '''Test if a pixel specified by its top-left corner is within bounds.
- Pos is the (x, y) coordinates of the top-left of the pixel.
- Bounds is (Left, Top, Right, Bottom) measured in pixel edges.
- '''
- x, y = Pos
- return (x >= Bounds[0] and x < Bounds[2] and
- y >= Bounds[1] and y < Bounds[3])
- #-------------------------------------------------------------------------------
- def FindFirstPixel(Pixels, Bounds, MatchValue, StartPos, DeltaPos):
- '''Find the first pixel of a given value (colour) in a given direction.
- If a matching pixel is found within Bounds, the position of that pixel
- (or rather its top-left corner) is returned. The pixel indicated by
- StartPos is included in the search.
- Pixels is the pixel access object provided by the load() method of an image.
- Bounds must already be clipped to the boundaries of the image.
- DeltaPos is in the form (dx, dy) just as StartPos is in the form (x, y).
- If no match is found within Bounds, None is returned.
- '''
- Result = None
- Pos = tuple(StartPos)
- while PixelInBounds(Pos, Bounds):
- if Pixels[Pos[0], Pos[1]] == MatchValue:
- Result = Pos
- break
- Pos = VSum(Pos, DeltaPos)
- return Result
- #-------------------------------------------------------------------------------
- def FindRunEnd(Pixels, Bounds, StartPos, DeltaPos):
- '''Find the last pixel in a run of colours within a rectangular region..
- if the pixel (whose top-left corner is) at StartPos is not within Bounds,
- None is returned. If the run reaches or extends beyond Bounds, the position
- of (the top-left corner of the) last pixel within the bounds is returned.
- Pixels is the pixel access object provided by the load() method of an image.
- Bounds must already be clipped to the boundaries of the image.
- Almost always, the only useful values DeltaPos for this function are (0, 1),
- (0, -1), (1, 0) or (-1, 0).
- '''
- Result = None
- if PixelInBounds(StartPos, Bounds):
- MatchValue = Pixels[StartPos[0], StartPos[1]]
- Pos = tuple(StartPos)
- while PixelInBounds(Pos, Bounds):
- if Pixels[Pos[0], Pos[1]] != MatchValue:
- Result = Pos
- break
- Pos = VSum(Pos, DeltaPos)
- return Result
- #-------------------------------------------------------------------------------
- # Vertical stitching functions
- #-------------------------------------------------------------------------------
- def FindStripBounds(Img):
- '''Find the area within a screenshot which contains the thread strip.'''
- #-----------------------------------------------------------------------------
- def StripEdgeCandidates(Pixels, Bounds, y, Dir):
- '''Along an image row, find likely comment strip edge locations.
- Dir may be:
- 1: Scan left to right and find left edges
- -1: Scan right to left and find right edges
- '''
- Result = []
- if PageBGColour in ((HeaderBGColour, CommentBGColour)):
- return Result
- BG = PageBGColour
- StepSize = 4
- StraddleSize = 8
- OutStepSize = 1
- InStepSize = 3
- VSpan = 16
- if Dir > 0:
- Step = StepSize
- Straddle = StraddleSize
- OutStep = -OutStepSize
- InStep = InStepSize
- x = Bounds[0]
- DeltaPos = vRight
- HVerifyRng = (InStep, Straddle)
- else:
- Step = -StepSize
- Straddle = -StraddleSize
- OutStep = OutStepSize
- InStep = -InStepSize
- x = Bounds[2] - 1
- DeltaPos = vLeft
- HVerifyRng = (-Straddle + 1, InStep + 1)
- n = (Bounds[2] - Bounds[0] - StraddleSize - 100) // StepSize
- v0 = max(Bounds[1], y - (VSpan // 2))
- v1 = min(Bounds[3], y - (VSpan // 2) + VSpan)
- VRng = (v0, v1)
- while n > 0:
- if Pixels[x, y] == BG:
- Col = Pixels[x + Straddle, y]
- if Col in (HeaderBGColour, CommentBGColour):
- x1 = FindRunEnd(Pixels, Bounds, (x, y), DeltaPos)[0]
- if all(Pixels[x1 + i, y] == Col for i in range(*HVerifyRng)):
- if all(Pixels[x1 + OutStep, i] == BG for i in range(*VRng)):
- if all(Pixels[x1 + InStep, i] == Col for i in range(*VRng)):
- Candidate = x1 + 1 if Step < 0 else x1
- Result.append(Candidate)
- x += Step
- n -= 1
- return Result
- #-----------------------------------------------------------------------------
- def FindStripEdge(Pixels, Bounds, Dir):
- '''Find a likely comment strip edge x value.
- Dir may be:
- 1: Scan left to right and find left edges
- -1: Scan right to left and find right edges
- Several image rows are scanned and a histogram is built. An attempt is
- made to select the comment strip edges but not the right side panel edges.
- '''
- Result = None
- if PageBGColour in ((HeaderBGColour, CommentBGColour)):
- return Result
- Right = Bounds[2]
- H = {}
- for y in range(Bounds[1] + 20, Bounds[3] - 20, 15):
- C = StripEdgeCandidates(Pixels, Bounds, y, Dir)
- for x in C:
- Weight = Right - x
- if x in H:
- H[x] += Weight
- else:
- H[x] = Weight
- if len(H) > 0:
- MaxWeight = max(H[x] for x in H)
- Result = min(x for x in H if H[x] == MaxWeight)
- return Result
- #-----------------------------------------------------------------------------
- Bounds = [0, 0] + list(Img.size)
- Pixels = Img.load()
- Top = None
- Bottom = None
- LBottom = None
- RBottom = None
- Left = FindStripEdge(Pixels, Bounds, 1)
- Right = FindStripEdge(Pixels, Bounds, -1)
- # Avoid the Facebook masthead, the bottom window border, the chat tab
- # at the lower right and possibly Firefox's URL overlay..
- if Left is not None:
- y = Img.size[1] // 2
- TopPos = FindRunEnd(Pixels, Bounds, (Left - 1, y), vUp)
- BotPos = FindRunEnd(Pixels, Bounds, (Left - 1, y), vDown)
- Top = TopPos[1] + 1 if TopPos is not None else None
- LBottom = BotPos[1] if BotPos is not None else None
- if Right is not None:
- y = Img.size[1] // 2
- BotPos = FindRunEnd(Pixels, Bounds, (Right + 2, y), vDown)
- RBottom = BotPos[1] if BotPos is not None else None
- if LBottom is not None:
- if RBottom is not None:
- Bottom = min(LBottom, RBottom)
- else:
- Bottom = LBottom
- else:
- Bottom = RBottom
- if Left is not None:
- Bounds[0] = Left
- if Right is not None:
- Bounds[2] = Right
- if Top is not None:
- Bounds[1] = Top
- if Bottom is not None:
- Bounds[3] = Bottom
- return tuple(Bounds)
- #-------------------------------------------------------------------------------
- def ImageVOverlaps(Img1, Img2, Tolerance=0):
- '''Measure how much overlap there is between two successive images.
- Because repetition in the images can mean multiple overlaps are valid,
- a list of overlap candidates is returned.
- If no overlap is found, an empty list is returned.
- Because scrolling in Firefox often changes the colours of some pixels
- in an image by one or two a few levels per channel, a tolerance of 3
- is recommended.
- '''
- Width = min(Img1.size[0], Img2.size[0])
- #-----------------------------------------------------------------------------
- def RowsAreMatching(Pixels1, y1, Pixels2, y2, x0, x1):
- '''Return True iff two image rows match within Tolerance.
- The horizontal bounds of the scan is defined by x0 <= x < x1
- where x is the coordinates of the upper-left of a pixel to test.
- '''
- Result = True
- for x in range(x0, x1):
- if Pixels1[x, y1] != Pixels2[x, y2]:
- Pix1 = Pixels1[x, y1]
- Pix2 = Pixels2[x, y2]
- Diff = VDiff(Pix1, Pix2)
- if max(abs(v) for v in Diff) > Tolerance:
- Result = False
- break
- return Result
- #-----------------------------------------------------------------------------
- Result = []
- Pixels1 = Img1.load()
- Height1 = Img1.size[1]
- Pixels2 = Img2.load()
- Height2 = Img2.size[1]
- MidX = Width // 2
- # Prepare the initial list of possible overlaps.
- for y1 in range(Height1):
- if RowsAreMatching(Pixels1, y1, Pixels2, 0, MidX, MidX + 1):
- Result.append(Height1 - y1)
- # Perform a quick screening test followed by a more thorough test
- # of the remaining candidates.
- for (StartY, x0, x1) in [(0, MidX, MidX + 1), (0, 0, Width)]:
- NewResult = []
- for Overlap in Result:
- y2 = StartY
- y1 = Height1 - Overlap + StartY
- IsMatching = True
- while y1 < Height1 and y2 < Height2:
- if not RowsAreMatching(Pixels1, y1, Pixels2, y2, x0, x1):
- IsMatching = False
- break
- y1 += 1
- y2 += 1
- if IsMatching:
- NewResult.append(Overlap)
- Result = NewResult
- return Result
- #-------------------------------------------------------------------------------
- def WeightedAverageOverlap(OverlapLists):
- '''Weighing greedy overlaps more, find the average weighted overlap.
- OverlapLists is a list of overlap lists. Most image pairs will have an
- overlap list of just one entry and most of the time, image pairs will
- have identical single-element lists. The weighted average overlap will
- be useful for selecting the best overlap candidate for tricky image pairs.
- '''
- Result = 0
- Acc = 0.0
- SumW = 0.0
- for Overlaps in OverlapLists:
- n = len(Overlaps)
- if n > 0:
- if n == 1:
- Acc += Overlaps[0]
- SumW += 1.0
- else:
- InnerAcc = 0.0
- InnerSumW = 0.0
- for i, v in enumerate(reversed(sorted(Overlaps))):
- w = 1.0 / float(1 + i)
- InnerAcc += float(v) * w
- InnerSumW += w
- Acc += InnerAcc / InnerSumW
- SumW += 1.0
- Result = Acc / SumW if SumW > 0 else 0.0
- return Result
- #-------------------------------------------------------------------------------
- def MostLikelyOverlaps(OverlapLists):
- '''Determine a single overlap value for each pair of successive images.
- Most of the time, each image pair has an overlap list of one element and
- that element is shared by the other overlap lists. For each image pair's
- overlap lists, the element closest to the weighted average overlap is
- selected.
- '''
- Result = []
- z = WeightedAverageOverlap(OverlapLists)
- for L in OverlapLists:
- Overlap = 0
- if len(L) > 0:
- Deviations = [abs(v - z) for v in L]
- Overlap = L[Deviations.index(min(Deviations))]
- Result.append(Overlap)
- return Result
- #-------------------------------------------------------------------------------
- def NonOverlappingImages(OverlappingImages, Overlaps):
- '''Create a list of non-overlapping images.
- In the case of 100% overlaps, images may be excluded from the list
- returned. Therefore the resulting image indices may be different.
- '''
- BMH = 6 # Break mark height
- Result = []
- Imgs = OverlappingImages
- if len(Imgs) > 0:
- Width = Imgs[0].size[0]
- # Trim overlaps and mark discontinuities
- NewImgs = []
- PrevHadBreak = False
- for ImgIx, Img in enumerate(Imgs):
- if ImgIx + 1 < len(Imgs):
- Overlap = Overlaps[ImgIx]
- CurrentHasBreak = Overlap < 1
- else:
- Overlap = 0
- CurrentHasBreak = False
- NewImg = None
- h = Img.size[1] - max(0, Overlap)
- if not (PrevHadBreak or CurrentHasBreak):
- if h > 0:
- NewImg = Img.crop((0, 0, Width, h))
- else:
- TopExtra = BMH // 2 if PrevHadBreak else 0
- BotExtra = BMH - (BMH // 2) if CurrentHasBreak else 0
- y0 = TopExtra
- y1 = TopExtra + h
- NewH = y1 + BotExtra
- NewImg = Image.new('RGB', (Width, NewH), PageBGColour)
- if h > 0:
- CroppedImg = Img.crop((0, 0, Width, h))
- NewImg.paste(CroppedImg, (0, y0))
- if PrevHadBreak:
- NewImg.paste(DiscontinuityColour, (0, 0, Width, y0))
- if CurrentHasBreak:
- NewImg.paste(DiscontinuityColour, (0, y1, Width, NewH))
- if NewImg is not None:
- NewImgs.append(NewImg)
- PrevHadBreak = CurrentHasBreak
- Result = NewImgs
- # We now have an array of perfectly stacked, non-overlapping images.
- # The images numbers may have changed due to deletions resulting
- # from 100% overlaps.
- return Result
- #-------------------------------------------------------------------------------
- def VTrimmedVStrip(VStrip):
- '''Create a VStrip image with the top and bottom non-thread bits trimmed.
- Facebook threads are separated from adjacent threads by regions of solid
- page background colour. This function looks for a breaks in the first
- image anf from there, the next break in order to determine which parts
- of the strip to return as a new tVStrip.
- '''
- #-----------------------------------------------------------------------------
- def IsBlankOrOOB(Pixels, Bounds, y):
- '''Return True is a scan line is blank or out of bounds.'''
- if y < Bounds[1] or y >= Bounds[3]:
- return True
- else:
- BG = PageBGColour
- return all(Pixels[x, y] == BG for x in range(Bounds[0], Bounds[2]))
- #-----------------------------------------------------------------------------
- NewImgs = []
- if VStrip.Height > 0:
- Img = VStrip.Imgs[0]
- Bounds = (0, 0) + (Img.size)
- Pixels = Img.load()
- Top = 0
- BotSearchStart = 0
- Pos = FindFirstPixel(Pixels, Bounds, PageBGColour, (0, 0), vDown)
- if Pos is not None:
- Pos = FindRunEnd(Pixels, Bounds, Pos, vDown)
- if Pos is not None:
- Top = Pos[1] + 1
- BotSearchStart = Top
- while not IsBlankOrOOB(Pixels, Bounds, Top - 1):
- Top -= 1
- if VStrip.Height > Top:
- Bounds = (0, 0, VStrip.Width, VStrip.Height)
- Bottom = VStrip.Height
- Pos = FindFirstPixel(VStrip, Bounds, PageBGColour,
- (0, BotSearchStart), vDown)
- if Pos is not None:
- Bottom = Pos[1]
- while not IsBlankOrOOB(VStrip, Bounds, Bottom):
- Bottom += 1
- for ImgIx in range(VStrip.Count):
- y0, y1 = VStrip.Fences[ImgIx : ImgIx + 2]
- c0, c1 = max(y0, Top), min(y1, Bottom)
- if c1 > c0:
- Img = VStrip.Imgs[ImgIx]
- if c1 == y0 and c1 == y1:
- NewImgs.append(Img)
- else:
- NewImgs.append(Img.crop((0, c0 - y0, VStrip.Width, c1 - y0)))
- Result = tVStrip(NewImgs)
- return Result
- #-------------------------------------------------------------------------------
- # Comment detection and layout
- #-------------------------------------------------------------------------------
- def FindCommentBreak(VStrip, StartPos, RightMargin, BottomMargin):
- '''Find the vertical span of an inter-comment space.
- Comments are assumed to begin with a squarish avatar image. The break is
- assumed to exist between the top of the avartar and the lowest non-blank
- scanline (within the thread strip box). In the case of Zalgo text, the
- break may be zero rows high.
- The x-value of StartPos should indicate the leftmost edge of each avatar.
- RightMargin and BottomMargin are measured from the origin.
- If a comment break is found, (Top, Bottom) is returned. If Top and Bottom
- are the same,the break has zero height. If no comment break is found, None
- is returned.
- '''
- BodyCols = [HeaderBGColour, CommentBGColour]
- #-----------------------------------------------------------------------------
- def IsDiscontinuity(y):
- return VStrip[max(0, StartPos[0] - 1), y] == DiscontinuityColour
- #-----------------------------------------------------------------------------
- def IsInAvatar(y):
- TestColumns = [StartPos[0], StartPos[0] + 1, StartPos[0] + 7]
- return any(VStrip[x, y] not in BodyCols for x in TestColumns)
- #-----------------------------------------------------------------------------
- def IsBlankLine(y, BGColour):
- return all(VStrip[x, y] == BGColour
- for x in range(StartPos[0], RightMargin))
- #-----------------------------------------------------------------------------
- Result = None
- BG = CommentBGColour
- x, y = StartPos
- while y < BottomMargin:
- if not IsDiscontinuity(y) and not IsInAvatar(y):
- BG = VStrip[x, y]
- break
- y += 1
- AvatarBottom = y
- if AvatarBottom < BottomMargin:
- while y < BottomMargin:
- if not IsDiscontinuity(y) and IsInAvatar(y):
- break
- y += 1
- NextAvatarTop = y
- if NextAvatarTop < BottomMargin:
- while y > AvatarBottom:
- if not IsDiscontinuity(y - 1) and not IsBlankLine(y - 1, BG):
- break
- y -= 1
- BreakTop = y
- Result = (BreakTop, NextAvatarTop)
- return Result
- #-------------------------------------------------------------------------------
- def FindCommentBreakMetrics(VStrip):
- '''Find the metrics of the thread to permit scanning for comment breaks.
- This function returns (Left, Right, Bottom, FirstBreak). If a comment
- break is found, FirstBreak will be the break's extent in (Top, Bottom)
- form, else None.
- '''
- BodyCols = [HeaderBGColour, CommentBGColour]
- #-----------------------------------------------------------------------------
- def IsAvatarAt(Pos, Bounds):
- '''Return True iff Pos marks the top of an avatar image.'''
- MaxWander = 120
- #---------------------------------------------------------------------------
- def IsMark(Pos):
- return VStrip[Pos] not in BodyCols
- #---------------------------------------------------------------------------
- Left = Right = Pos[0]
- Top = Bottom = Pos[1]
- LBounds = (
- max(Bounds[0], Pos[0] - MaxWander * 1 // 4),
- max(Bounds[1], Top - 1),
- min(Bounds[2], Pos[0] + MaxWander * 3 // 4),
- min(Bounds[3], Top + MaxWander)
- )
- Result = IsMark((Pos[0], Top))
- Width = 1
- Height = 1
- if Result:
- # Check the top edge.
- Result = False
- while Left >= LBounds[0] and IsMark((Left, Top)):
- Left -= 1
- if Left >= LBounds[0]:
- while Right < LBounds[2] and IsMark((Right, Top)):
- Right += 1
- if Right < LBounds[2]:
- Width = Right - Left
- if Width > 22:
- if not any(IsMark((x, Top - 1)) for x in range(Left, Right + 1)):
- Result = True
- Left += 1
- if Result:
- # Check the left and right edges.
- Result = False
- Bottom = Top + 1
- while Bottom < LBounds[3] and IsMark((Left, Bottom)):
- Bottom += 1
- Height = Bottom - Top
- if Bottom < LBounds[3] and 0.9 < float(Width) / Height < 1.1:
- if not any(IsMark((Left - 1, y)) for y in range(Top, Bottom)):
- if all(IsMark((Right - 1, y)) for y in range(Top, Bottom)):
- if not any(IsMark((Right, y)) for y in range(Top, Bottom)):
- Result = True
- if Result:
- # Check the bottom edge.
- Result = False
- if all(IsMark((x, Bottom - 1)) for x in range(Left, Right)):
- if not any(IsMark((x, Bottom)) for x in range(Left - 1, Right + 1)):
- Result = True
- return Result
- #-----------------------------------------------------------------------------
- Result = None
- XSearchLimit = min(100, VStrip.Width - 100)
- YSearchLimit = VStrip.Height - 20
- # Find the left margin, clear of the strip box edge colours.
- x = 0
- y = 10
- while VStrip[x, y] not in BodyCols and x + 50 < VStrip.Width:
- x += 1
- x += 2
- LeftMargin = x
- # Skip past the thread header.
- Col = VStrip[x, y]
- if Col == HeaderBGColour:
- while y + 20 < VStrip.Height and VStrip[x, y] == Col:
- y += 10
- # Search for an avatar image by dropping fishing lines, each line several
- # pixels ahead of the next one to the right.
- FeelerVSpacing = 1000
- FeelerHSpacing = 16
- SearchBounds = (x, y, XSearchLimit, YSearchLimit)
- Feelers = []
- x += 8
- while x < XSearchLimit:
- Feelers.append((x, y))
- x += FeelerHSpacing
- AvatarPos = None
- NumTests = 0
- while AvatarPos is None and len(Feelers) > 0:
- LastFeelerY = None
- FIx = 0
- while FIx < len(Feelers):
- (x, y) = Feelers[FIx]
- if LastFeelerY is not None and y > LastFeelerY - FeelerVSpacing:
- break
- NumTests += 1
- FoundAvatar = IsAvatarAt((x, y), SearchBounds)
- if FoundAvatar:
- while VStrip[x - 1, y] not in BodyCols:
- x -= 1
- AvatarPos = (x, y)
- break
- y += 1
- Feelers[FIx] = (x, y)
- LastFeelerY = y
- if y < YSearchLimit:
- FIx += 1
- else:
- del Feelers[0]
- LastFeelerY = None
- if AvatarPos is not None:
- RightMargin = VStrip.Width - LeftMargin
- BottomMargin = max(AvatarPos[1], VStrip.Height - 80)
- FirstBreak = FindCommentBreak(VStrip, AvatarPos, RightMargin, BottomMargin)
- Result = (AvatarPos[0], RightMargin, BottomMargin, FirstBreak)
- return Result
- #-------------------------------------------------------------------------------
- def WrappedCommentRuns(
- CmtExts, CapHeight, Margin, Gutter, TargetHeight, StripWidth
- ):
- '''Neatly wrap comment runs, top justified.
- CmtExts is a list of ((SpaceTop, SpaceBottom), (CommentTop, CommentBottom)).
- To allow room for caps to be attached to the ends of comment breaks
- which fall between columns, CapHeight must be specified.
- This function returns (MCSize, Pastes) where MCSize is (Width, Height) and
- Pastes is a list of (DestPos, SourceBox).
- '''
- Result = None
- ContentHeight = TargetHeight - 2 * Margin
- # Perform basic wrapping.
- CSMs = [(0, CmtExts[0][1] - CmtExts[0][0])]
- for i in range(1, len(CmtExts)):
- CSMs.append(
- (CmtExts[i][0] - CmtExts[i - 1][1], CmtExts[i][1] - CmtExts[i][0])
- )
- J = []
- y = 0
- Column = []
- for i, CSM in enumerate(CSMs):
- TopCapH = 0 if i == 0 else CapHeight
- BotCapH = 0 if i + 1 == len(CSMs) else CapHeight
- Space, Mark = CSM
- if y == 0:
- if y + TopCapH + Mark + BotCapH > ContentHeight:
- y = TopCapH + Mark
- Column.append((i, (TopCapH, y)))
- J.append(Column)
- Column = []
- y = 0
- else:
- y = TopCapH + Mark
- Column.append((i, (TopCapH, y)))
- else:
- if y + Space + Mark + BotCapH > ContentHeight:
- J.append(Column)
- TopCapH = CapHeight
- y = TopCapH + Mark
- Column = [(i, (TopCapH, y))]
- else:
- Column.append((i, (y + Space, y + Space + Mark)))
- y += Space + Mark
- if len(Column) > 0:
- J.append(Column)
- Column = []
- # Attempt to smooth the bottom edge.
- Jiggled = True
- while Jiggled:
- Jiggled = False
- for i in range(len(J) - 2):
- Col2IsLast = i + 1 == len(J)
- Col1 = J[i]
- Col2 = J[i + 1]
- if len(Col1) > 1:
- Bot1 = Col1[-1][1][1] + CapHeight
- Bot2 = Col2[-1][1][1] + (0 if Col2IsLast else CapHeight)
- if Bot1 > Bot2:
- Last1Ix = Col1[-1][0]
- First2Ix = Col2[0][0]
- Last1SM = sum(CSMs[Last1Ix])
- DeltaBot2 = CSMs[Last1Ix][1] + CSMs[First2Ix][0]
- NewBot1 = Bot1 - Last1SM
- NewBot2 = DeltaBot2 + Bot2
- if NewBot2 <= ContentHeight and abs(NewBot1 - NewBot2) < Bot1 - Bot2:
- J[i].pop()
- NewC2 = [(Last1Ix, (CapHeight, CapHeight + CSMs[Last1Ix][1]))]
- for (CEIx, (CTop, CBot)) in J[i + 1]:
- NewC2.append((CEIx, (CTop + DeltaBot2, CBot + DeltaBot2)))
- J[i + 1] = NewC2
- Jiggled = True
- # Find the final height of the multicolumn image.
- JHeight = 0
- BotCapH = 0
- for Col in reversed(J):
- JHeight = max(JHeight, Col[-1][1][1] + BotCapH)
- BotCapH = CapHeight
- JHeight += 2 * Margin
- # Position comment image blocks.
- Pastes = []
- x = Margin
- for i, JCol in enumerate(J):
- CmtIx0, VRange0 = JCol[0]
- CmtIx1, VRange1 = JCol[-1]
- VSRange = (CmtExts[CmtIx0][0], CmtExts[CmtIx1][1])
- Pos = (x, Margin + VRange0[0])
- VSBox = (0, VSRange[0], StripWidth, VSRange[1])
- Pastes.append((Pos, VSBox))
- x += StripWidth + Gutter
- JWidth = x - Gutter + Margin
- Result = ((JWidth, JHeight), Pastes)
- return Result
- #-------------------------------------------------------------------------------
- # Image modification functions
- #-------------------------------------------------------------------------------
- def SuppressNewCommentsMarker(VStrip):
- '''Erase the highlighting along the left edge which indicate new comments.'''
- #-----------------------------------------------------------------------------
- def GetReplacementColours(y, dy):
- '''Search vertically for a patch of pixels to use to replace the NCM.'''
- Result = None
- if y + dy >= 0 and y + dy < VStrip.Height:
- x = 0
- Col = VStrip[x, y]
- if Col == NewCommentMarkerColour:
- Result = []
- while x < 12 and Col == NewCommentMarkerColour:
- Result.append(VStrip[x, y + dy])
- x += 1
- Col = VStrip[x, y]
- return Result
- #-----------------------------------------------------------------------------
- def EraseNCM(y, Patch):
- '''Erase the new comment marker pixels at the given row.'''
- for i, Col in enumerate(Patch):
- VStrip[i, y] = Col
- #-----------------------------------------------------------------------------
- Patch = None
- NCMBottom = 0
- for y in reversed(range(VStrip.Height)):
- if VStrip[0, y] == NewCommentMarkerColour:
- if Patch is None:
- Patch = GetReplacementColours(y, 1)
- if Patch is not None:
- EraseNCM(y, Patch)
- else:
- NCMBottom = y + 1
- break
- if NCMBottom > 0:
- NCMTop = NCMBottom
- for y in reversed(range(NCMBottom)):
- if VStrip[0, y] != NewCommentMarkerColour:
- if Patch is None:
- Patch = GetReplacementColours(y + 1, -1)
- NCMTop = y + 1
- break
- if Patch is not None:
- for y in range(NCMTop, NCMBottom):
- EraseNCM(y, Patch)
- #-------------------------------------------------------------------------------
- def NewCapImgs(VStrip, CmtBreakSpan, CapHeight):
- '''Create top and bottom cap images to tidy column breaks.'''
- #-----------------------------------------------------------------------------
- def LerpCols(ColA, ColB, Fade):
- '''Linearly interpolate between two 8-bit-per-channel RGB' colours.
- Fade is the interpolation parameter 0.0..1.0 for ColA..ColB.
- '''
- Result = []
- Gamma = 1.0/2.2
- InvGamma = 1.0/Gamma
- Inv255 = 1.0/255.0
- for (LevA, LevB) in zip(ColA, ColB):
- a = (float(LevA) * Inv255) ** InvGamma
- b = (float(LevB) * Inv255) ** InvGamma
- r = a + Fade * (b - a)
- Result.append(int(round(max(0.0, min(1.0, r)) ** Gamma * 255.0)))
- return tuple(Result)
- #-----------------------------------------------------------------------------
- TopCap = Image.new("RGB", (VStrip.Width, CapHeight), CommentBGColour)
- if CmtBreakSpan is not None:
- Box = (0, CmtBreakSpan[0], VStrip.Width, CmtBreakSpan[1])
- y = 0
- while y < CapHeight:
- h = min(CapHeight - y, Box[3] - Box[1])
- Box = Box[:3] + (Box[1] + h,)
- VStrip.PasteTo(TopCap, (0, y), Box)
- y += h
- # The Image copy() method is buggy. It provides a copy with a
- # defective pixel access method.
- # Instead, paste the whole image into a new image.
- BotCap = Image.new("RGB", (VStrip.Width, CapHeight))
- BotCap.paste(TopCap, (0, 0))
- TopPixels = TopCap.load()
- BotPixels = BotCap.load()
- LeftCol = TopPixels[0, 0]
- RightCol = TopPixels[VStrip.Width - 1, 0]
- TopCol = TopCapColour
- BotCol = BottomCapColour
- for x in range(1, VStrip.Width - 1):
- TopPixels[x, 0] = TopCol
- BotPixels[x, CapHeight - 1] = BotCol
- TopPixels[0, 0] = LerpCols(LeftCol, TopCol, 0.5)
- TopPixels[VStrip.Width - 1, 0] = LerpCols(RightCol, TopCol, 0.5)
- BotPixels[0, CapHeight - 1] = LerpCols(LeftCol, BotCol, 0.5)
- BotPixels[VStrip.Width - 1, CapHeight - 1] = LerpCols(RightCol, BotCol, 0.5)
- return TopCap, BotCap
- #-------------------------------------------------------------------------------
- # Main
- #-------------------------------------------------------------------------------
- def Main():
- #-----------------------------------------------------------------------------
- def CheckArgRange(Name, Value, MinValue, MaxValue):
- if not MinValue <= Value <= MaxValue:
- raise ArgError('argument ' + Name + ': out of range ' + #<<<<<<<<<
- str(MinValue) + '..' + str(MaxValue) +'.')
- #-----------------------------------------------------------------------------
- def PrettyBoundsStr(B):
- return 'x: %d..%d y: %d..%d' % (B[0], B[2], B[1], B[3])
- #-----------------------------------------------------------------------------
- def GetArguments():
- cn = os.path.basename(sys.argv[0])
- Parser = argparse.ArgumentParser(
- prog=cn,
- add_help=False,
- description='Stitches a series of screenshots of a Facebook thread.'
- )
- Parser.add_argument(
- '-h', '--help',
- dest='Help', action='store_true',
- help='Display this message and exit.')
- Parser.add_argument(
- '-d', '--dir', metavar='DIR',
- dest='Dir', default='',
- help=('Specify the directory where the screenshot image files may ' +
- 'be found. The default is the current directory.'))
- Parser.add_argument(
- '-f', '--name-format', metavar='FMT',
- dest='NameFormat', default='%02d.png',
- help=('Specify the C format string for the numbered image ' +
- 'file names (starting at 1). The default is "%%02d.png".'))
- Parser.add_argument(
- '-g', '--gutter',
- dest='Gutter', type=int, default=12,
- help='Specify the space in pixels to insert between columns.')
- Parser.add_argument(
- '-y', '--height',
- dest='Height', type=int,
- help=('Specify the target height in pixels of the output image.' +
- 'This may be used correct the appearance of an unsightly short ' +
- 'column.'))
- Parser.add_argument(
- '-m', '--margin',
- dest='Margin', type=int, default=16,
- help=('Specify the space in pixels to put between the image edges ' +
- 'and the comment columns.'))
- Parser.add_argument(
- '-r', '--remove-ncm',
- dest='RemoveNCM', action='store_true',
- help='Remove the new comment marker.')
- Parser.add_argument(
- '-t', '--tolerance',
- dest='Tolerance', type=int, default=3,
- help=('Specify the maximum difference per channel to accept ' +
- 'two pixels as matching.'))
- Parser.add_argument(
- '-q', '--quiet',
- dest='Quiet', action='store_true',
- help='Suppress all output.')
- Parser.add_argument(
- '-v', '--verbose',
- dest='Verbose', action='store_true',
- help='Display detailed metrics and progress information.')
- Parser.add_argument(
- '-V', '--version',
- dest='Version', action='store_true',
- help='Display version and exit.')
- Parser.add_argument(
- 'NumScreenshots', metavar='number-of-screenshots',
- type=int,
- help=('Specify the number of screenshot files to load. If the ' +
- 'default format string is used, the files must be named ' +
- '01.png, 02.png, 03.png and so on.'))
- Parser.add_argument(
- 'OutputFileName', metavar='output-file',
- help='Specify the name of the multicolumn PNG image file to output.')
- if '-h' in sys.argv or '--help' in sys.argv:
- Parser.print_help()
- print(
- '\nExamples:\n' +
- ' ' + cn + ' 29 mc.png\n' +
- ' ' + cn + ' --margin 12 --gutter 4 -f "scr_%03d.png" 110 mc.png\n' +
- ' ' + cn + ' --tolerance 5 --height 2440 --remove-ncm 42 mc.png\n' +
- ' ' + cn + ' -v -y 2440 -t 5 -g 4 22 mc.png'
- )
- sys.exit(0)
- if '-V' in sys.argv or '--version' in sys.argv:
- print(VersionStr)
- sys.exit(0)
- Args = Parser.parse_args()
- if Args.OutputFileName == '-':
- # Write multicolumn image to stdout.
- Args.IsQuiet = True
- setattr(Args, 'Verbosity', 0 if Args.Quiet else (2 if Args.Verbose else 1))
- if Args.Height is not None:
- CheckArgRange('-y/--height', Args.Height, 200, 32000)
- CheckArgRange('-m/--margin', Args.Margin, 0, 100)
- CheckArgRange('-g/--gutter', Args.Gutter, 0, 100)
- CheckArgRange('-t/--tolerance', Args.Tolerance, 0, 64)
- CheckArgRange('number-of-screenshots', Args.NumScreenshots, 1, 999)
- try:
- S = Args.NameFormat % (1)
- except (TypeError, ValueError):
- raise ArgError('argument -f/--name-format: Invalid format string.')
- return Args
- #-----------------------------------------------------------------------------
- Result = 0
- ErrMsg = ''
- CmdName = os.path.basename(sys.argv[0])
- try:
- Args = GetArguments()
- Verbosity = Args.Verbosity
- Margin = Args.Margin
- Gutter = Args.Gutter
- TargetHeight = Args.Height
- Tolerance = Args.Tolerance
- RemoveNCM = Args.RemoveNCM
- n = Args.NumScreenshots
- OutputFileName = Args.OutputFileName
- if Args.Dir != '' and not os.path.isdir(Args.Dir):
- raise ArgError('argument --dir must be a directory.')
- if os.path.isdir(OutputFileName):
- raise ArgError('argument output-file must not be a directory.')
- else:
- Base = os.path.basename(OutputFileName)
- if Base in ['', '.', '..', '~']:
- raise ArgError('argument output-file "' + OutputFileName +
- '" is invalid.')
- ImgNames = [
- os.path.join(Args.Dir, Args.NameFormat % (i)) for i in range(1, n + 1)
- ]
- if Verbosity >= 1:
- print('Loading ' + str(n) + ' images...')
- FirstImg = Image.open(ImgNames[0]).convert('RGB')
- FirstImgFmt = FirstImg.format
- if Verbosity >= 2:
- print(' Identifying thread strip window boundaries...')
- Bounds = FindStripBounds(FirstImg)
- if Verbosity >= 2:
- print(' Bounds: ' + PrettyBoundsStr(Bounds) + ' (pixel edges)')
- if Verbosity >= 2:
- print(' Loading and cropping subsequent images...')
- Imgs = [FirstImg.crop(Bounds)]
- FirstImg = None
- for Name in ImgNames[1:]:
- Imgs.append(Image.open(Name).crop(Bounds).convert('RGB'))
- if Verbosity >= 1:
- print('Finding overlaps (Tolerance = ' + str(Tolerance) + ')...')
- OverlapLists = []
- for i in range(n - 1):
- OverlapList = ImageVOverlaps(Imgs[i], Imgs[i + 1], Tolerance)
- if len(OverlapList) < 1:
- if Verbosity >= 1:
- print(' No overlap between %d and %d.' % (i + 1, i + 2))
- OverlapLists.append(OverlapList)
- Overlaps = MostLikelyOverlaps(OverlapLists)
- Imgs = NonOverlappingImages(Imgs, Overlaps)
- VStrip = tVStrip(Imgs)
- VStrip = VTrimmedVStrip(VStrip)
- if RemoveNCM:
- if Verbosity >= 2:
- print('Removing highlighting for new comments...')
- SuppressNewCommentsMarker(VStrip)
- if Verbosity >= 1:
- print('Finding and laying out comment spans...')
- CBM = FindCommentBreakMetrics(VStrip)
- if CBM is None:
- raise ScanningError('Failed to analyse image for comment metrics.')
- Left, Right, Bottom, FirstBreak = CBM
- CapHeight = Left + 1
- if Verbosity >= 2:
- print((' Comment metrics: Left = %d, Right = %d, Bottom = %d' %
- (Left, Right, Bottom)))
- if FirstBreak is not None:
- print((' First comment break: (%d..%d)' %
- (FirstBreak[0], FirstBreak[1])))
- else:
- print((' No comment break detected!'))
- CmtExts = []
- y = 0
- CmtBreak = FirstBreak
- while CmtBreak is not None:
- CmtExts.append((y, CmtBreak[0]))
- y = CmtBreak[1]
- CmtBreak = FindCommentBreak(VStrip, (Left, y), Right, Bottom)
- if y < VStrip.Height:
- CmtExts.append((y, VStrip.Height))
- TailCmtHeights = [CE[1] - CE[0] for CE in CmtExts[1:]]
- if len(TailCmtHeights) >= 1:
- AvgCmtHeight = int(round(
- float(sum(TailCmtHeights)) / len(TailCmtHeights)))
- else:
- AvgCmtHeight = 10
- if TargetHeight is None:
- a = 4.0/3.0
- w = VStrip.Width + Gutter
- h = w * sqrt((float(VStrip.Height) / w) / a)
- h += 0.5 * AvgCmtHeight + 2 * CapHeight
- TargetHeight = int(round(h + 2 * Margin))
- if Verbosity >= 2:
- print((' Flowing columns for a target height of %d pixels.' %
- (TargetHeight)))
- MCSize, Pastes = WrappedCommentRuns(
- CmtExts, CapHeight, Margin, Gutter, TargetHeight, VStrip.Width
- )
- LPos, LBox = Pastes[-1]
- LastColBottom = LPos[1] + LBox[3] - LBox[1]
- LastColFraction = float(LastColBottom) / (MCSize[1] - 2 * Margin)
- LCFStr = str(int(round(100.0 * LastColFraction))) + '%'
- if Verbosity >= 2:
- print(' Columns spanned: ' + str(len(Pastes)))
- if LastColFraction < 0.2:
- if Verbosity >= 1:
- print(' Note: The last column is only ' + LCFStr + ' filled!')
- elif LastColFraction < 0.7:
- if Verbosity >= 1:
- print(' Note: The last column is ' + LCFStr + ' filled.')
- ImgSizeStr = str(MCSize[0]) + 'x' + str(MCSize[1])
- AnImgSizeStr = ('an' if ImgSizeStr[0] == '8' else 'a') + ' ' + ImgSizeStr
- if MCSize[0] > 32000 or MCSize[1] > 32000:
- raise LimitError('Output image would be too large at ' +
- ImgSizeStr +'.')
- if Verbosity >= 1:
- print('Assembling ' + AnImgSizeStr + ' multicolumn image...')
- if Verbosity >= 2:
- print(' Aspect ratio: %.3g' % (float(MCSize[0]) / MCSize[1]))
- TopCap, BotCap = NewCapImgs(VStrip, FirstBreak, CapHeight)
- MC = Image.new("RGB", MCSize, PageBGColour)
- for i, (Pos, Box) in enumerate(Pastes):
- VStrip.PasteTo(MC, Pos, Box)
- if i > 0:
- MC.paste(TopCap, (Pos[0], Pos[1] - CapHeight))
- if i + 1 < len(Pastes):
- MC.paste(BotCap, (Pos[0], Pos[1] + Box[3] - Box[1]))
- Fmt = FirstImgFmt if FirstImgFmt is not None else 'PNG'
- Ext = os.path.splitext(OutputFileName)[1].lower()
- Fmt = 'JPEG' if Ext in ['.jpg', '.jpeg'] else Fmt
- Fmt = 'GIF' if Ext in ['.gif'] else Fmt
- Fmt = 'PNG' if Ext in ['.png'] else Fmt
- Fmt = 'BMP' if Ext in ['.bmp'] else Fmt
- Fmt = 'TIFF' if Ext in ['.tif', '.tiff'] else Fmt
- if Verbosity >= 1:
- print(('Saving %s %s image to "%s"...' %
- (AnImgSizeStr, Fmt, OutputFileName)))
- MC.save(OutputFileName, Fmt)
- if Verbosity >= 2:
- print('Done!')
- except (ArgError) as E:
- ErrMsg = 'error: ' + str(E)
- Result = 2
- except (FileError) as E:
- ErrMsg = str(E)
- Result = 3
- except (LimitError) as E:
- ErrMsg = str(E)
- Result = 4
- except (ScanningError) as E:
- ErrMsg = str(E)
- Result = 5
- except (Exception) as E:
- exc_type, exc_value, exc_traceback = sys.exc_info()
- ErrLines = traceback.format_exc().splitlines()
- ErrMsg = 'Unhandled exception:\n' + '\n'.join(ErrLines)
- Result = 1
- if ErrMsg != '':
- print(CmdName + ': ' + ErrMsg, file=sys.stderr)
- return Result
- #-------------------------------------------------------------------------------
- # Command line trigger
- #-------------------------------------------------------------------------------
- if __name__ == '__main__':
- sys.exit(Main())
- #-------------------------------------------------------------------------------
- # End
- #-------------------------------------------------------------------------------
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement