mirror of
https://github.com/ciromattia/kcc
synced 2025-12-13 09:46:25 +00:00
General refactoring and tweaks
This commit is contained in:
@@ -1335,14 +1335,10 @@ class KCCGUI_MetaEditor(KCC_MetaEditor_ui.Ui_MetaEditorDialog):
|
||||
self.EditorFrame.setEnabled(True)
|
||||
self.OKButton.setEnabled(True)
|
||||
self.StatusLabel.setText('Separate authors with a comma.')
|
||||
self.SeriesLine.setText(self.parser.data['Series'])
|
||||
self.VolumeLine.setText(self.parser.data['Volume'])
|
||||
self.NumberLine.setText(self.parser.data['Number'])
|
||||
self.WriterLine.setText(', '.join(self.parser.data['Writers']))
|
||||
self.PencillerLine.setText(', '.join(self.parser.data['Pencillers']))
|
||||
self.InkerLine.setText(', '.join(self.parser.data['Inkers']))
|
||||
self.ColoristLine.setText(', '.join(self.parser.data['Colorists']))
|
||||
self.MUidLine.setText(self.parser.data['MUid'])
|
||||
for field in (self.SeriesLine, self.VolumeLine, self.NumberLine, self.MUidLine):
|
||||
field.setText(self.parser.data[field.objectName()[:-4]])
|
||||
for field in (self.WriterLine, self.PencillerLine, self.InkerLine, self.ColoristLine):
|
||||
field.setText(', '.join(self.parser.data[field.objectName()[:-4] + 's']))
|
||||
|
||||
def saveData(self):
|
||||
for field in (self.VolumeLine, self.NumberLine, self.MUidLine):
|
||||
|
||||
@@ -75,6 +75,7 @@ def main(argv=None):
|
||||
def buildHTML(path, imgfile, imgfilepath, forcePV=False):
|
||||
imgfilepath = md5Checksum(imgfilepath)
|
||||
filename = getImageFileName(imgfile)
|
||||
additionalStyle = ''
|
||||
if options.imgproc:
|
||||
if "Rotated" in options.imgIndex[imgfilepath]:
|
||||
rotatedPage = True
|
||||
@@ -92,6 +93,8 @@ def buildHTML(path, imgfile, imgfilepath, forcePV=False):
|
||||
noVerticalPV = True
|
||||
else:
|
||||
noVerticalPV = False
|
||||
if "BlackFill" in options.imgIndex[imgfilepath]:
|
||||
additionalStyle = ' style="background-color:#000000" '
|
||||
else:
|
||||
rotatedPage = False
|
||||
noPV = False
|
||||
@@ -125,7 +128,7 @@ def buildHTML(path, imgfile, imgfilepath, forcePV=False):
|
||||
"<link href=\"", "../" * (backref - 1),
|
||||
"style.css\" type=\"text/css\" rel=\"stylesheet\"/>\n",
|
||||
"</head>\n",
|
||||
"<body>\n",
|
||||
"<body" + additionalStyle + ">\n",
|
||||
"<div class=\"fs\">\n",
|
||||
"<div><img src=\"", "../" * backref, "Images/", postfix, imgfile, "\" alt=\"",
|
||||
imgfile, "\" class=\"singlePage\"/></div>\n"
|
||||
@@ -169,7 +172,7 @@ def buildHTML(path, imgfile, imgfilepath, forcePV=False):
|
||||
f.writelines(["<div id=\"" + boxes[i] + "\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
|
||||
"'{\"targetId\":\"" + boxes[i] + "-Panel-Parent\", \"ordinal\":" + str(order[i]),
|
||||
"}'></a></div>\n"])
|
||||
if options.quality == 2:
|
||||
if options.quality == 2 and not forcePV:
|
||||
imgfilepv = imgfile.split(".")
|
||||
imgfilepv[0] += "-hq"
|
||||
imgfilepv = ".".join(imgfilepv)
|
||||
@@ -433,6 +436,7 @@ def buildEPUB(path, chapterNames, tomeNumber):
|
||||
cover = os.path.join(os.path.join(path, 'OEBPS', 'Images'),
|
||||
'cover' + getImageFileName(filelist[-1][1])[1])
|
||||
image.Cover(os.path.join(filelist[-1][0], filelist[-1][1]), cover, options, tomeNumber)
|
||||
# Hack that force Panel View on at last one page
|
||||
if lastfile and not options.panelviewused and 'Ko' not in options.profile \
|
||||
and options.profile not in ['K1', 'K2', 'KDX', 'OTHER']:
|
||||
filelist[-1] = buildHTML(lastfile[0], lastfile[1], lastfile[2], True)
|
||||
@@ -444,24 +448,17 @@ def buildEPUB(path, chapterNames, tomeNumber):
|
||||
buildOPF(path, options.title, filelist, cover)
|
||||
|
||||
|
||||
def imgOptimization(img, opt, hqImage=None):
|
||||
def imgOptimization(img, opt):
|
||||
if not img.fill:
|
||||
img.getImageFill()
|
||||
if not opt.webtoon:
|
||||
img.cropWhiteSpace()
|
||||
if opt.cutpagenumbers and not opt.webtoon:
|
||||
img.cutPageNumber()
|
||||
img.optimizeImage()
|
||||
if hqImage:
|
||||
img.resizeImage(0)
|
||||
img.calculateBorder(hqImage, True)
|
||||
else:
|
||||
img.resizeImage()
|
||||
if opt.panelview:
|
||||
if opt.quality == 0:
|
||||
img.calculateBorder(img)
|
||||
elif opt.quality == 1:
|
||||
img.calculateBorder(img, True)
|
||||
img.autocontrastImage()
|
||||
img.resizeImage()
|
||||
if not img.second and opt.panelview:
|
||||
img.calculateBorder()
|
||||
if opt.forcepng and not opt.forcecolor:
|
||||
img.quantizeImage()
|
||||
|
||||
@@ -523,10 +520,6 @@ def imgFileProcessing(work):
|
||||
opt = work[2]
|
||||
output = []
|
||||
img = image.ComicPage(os.path.join(dirpath, afile), opt)
|
||||
if opt.quality == 2:
|
||||
wipe = False
|
||||
else:
|
||||
wipe = True
|
||||
if opt.nosplitrotate:
|
||||
splitter = None
|
||||
else:
|
||||
@@ -534,36 +527,30 @@ def imgFileProcessing(work):
|
||||
if splitter is not None:
|
||||
img0 = image.ComicPage(splitter[0], opt)
|
||||
imgOptimization(img0, opt)
|
||||
output.append(img0.saveToDir(dirpath))
|
||||
if not img0.noHQ:
|
||||
output.append(img0.saveToDir(dirpath))
|
||||
img1 = image.ComicPage(splitter[1], opt)
|
||||
imgOptimization(img1, opt)
|
||||
output.append(img1.saveToDir(dirpath))
|
||||
if wipe:
|
||||
output.append(img0.origFileName)
|
||||
output.append(img1.origFileName)
|
||||
if not img1.noHQ:
|
||||
output.append(img1.saveToDir(dirpath))
|
||||
output.extend([img.origFileName, img0.origFileName, img1.origFileName])
|
||||
if opt.quality == 2:
|
||||
img0b = image.ComicPage(splitter[0], opt, img0.fill)
|
||||
imgOptimization(img0b, opt, img0)
|
||||
output.extend([img0.origFileName, img1.origFileName])
|
||||
img0b = image.ComicPage(splitter[0], opt, img0)
|
||||
imgOptimization(img0b, opt)
|
||||
output.append(img0b.saveToDir(dirpath))
|
||||
img1b = image.ComicPage(splitter[1], opt, img1.fill)
|
||||
imgOptimization(img1b, opt, img1)
|
||||
img1b = image.ComicPage(splitter[1], opt, img1)
|
||||
imgOptimization(img1b, opt)
|
||||
output.append(img1b.saveToDir(dirpath))
|
||||
output.append(img0.origFileName)
|
||||
output.append(img1.origFileName)
|
||||
output.append(img.origFileName)
|
||||
else:
|
||||
output.append(img.origFileName)
|
||||
imgOptimization(img, opt)
|
||||
output.append(img.saveToDir(dirpath))
|
||||
if wipe:
|
||||
output.append(img.origFileName)
|
||||
if not img.noHQ:
|
||||
output.append(img.saveToDir(dirpath))
|
||||
if opt.quality == 2:
|
||||
img2 = image.ComicPage(os.path.join(dirpath, afile), opt, img.fill)
|
||||
if img.rotated:
|
||||
img2.image = img2.image.rotate(90, Image.BICUBIC, True)
|
||||
img2.rotated = True
|
||||
imgOptimization(img2, opt, img)
|
||||
img2 = image.ComicPage(os.path.join(dirpath, afile), opt, img)
|
||||
imgOptimization(img2, opt)
|
||||
output.append(img2.saveToDir(dirpath))
|
||||
output.append(img.origFileName)
|
||||
return output
|
||||
except Exception:
|
||||
return str(sys.exc_info()[1])
|
||||
@@ -575,7 +562,7 @@ def getWorkFolder(afile):
|
||||
if os.path.isdir(afile):
|
||||
workdir = mkdtemp('', 'KCC-TMP-')
|
||||
try:
|
||||
os.rmdir(workdir) # needed for copytree() fails if dst already exists
|
||||
os.rmdir(workdir)
|
||||
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
|
||||
if len(fullPath) > 240:
|
||||
raise UserWarning("Path is too long.")
|
||||
@@ -906,20 +893,20 @@ def detectMargins(path):
|
||||
yu = flag[2]
|
||||
xr = flag[3]
|
||||
yd = flag[4]
|
||||
if xl != "0":
|
||||
xl = "-" + str(float(xl)/100) + "%"
|
||||
if xl != "0.0":
|
||||
xl = "-" + xl + "%"
|
||||
else:
|
||||
xl = "0%"
|
||||
if xr != "0":
|
||||
xr = "-" + str(float(xr)/100) + "%"
|
||||
if xr != "0.0":
|
||||
xr = "-" + xr + "%"
|
||||
else:
|
||||
xr = "0%"
|
||||
if yu != "0":
|
||||
yu = "-" + str(float(yu)/100) + "%"
|
||||
if yu != "0.0":
|
||||
yu = "-" + yu + "%"
|
||||
else:
|
||||
yu = "0%"
|
||||
if yd != "0":
|
||||
yd = "-" + str(float(yd)/100) + "%"
|
||||
if yd != "0.0":
|
||||
yd = "-" + yd + "%"
|
||||
else:
|
||||
yd = "0%"
|
||||
return xl, yu, xr, yd
|
||||
@@ -1123,10 +1110,7 @@ def makeBook(source, qtGUI=None):
|
||||
getComicInfo(os.path.join(path, "OEBPS", "Images"), source)
|
||||
detectCorruption(os.path.join(path, "OEBPS", "Images"), source)
|
||||
if options.webtoon:
|
||||
if options.customheight > 0:
|
||||
comic2panel.main(['-y ' + str(options.customheight), '-i', '-m', path], qtGUI)
|
||||
else:
|
||||
comic2panel.main(['-y ' + str(image.ProfileData.Profiles[options.profile][1][1]), '-i', '-m', path], qtGUI)
|
||||
comic2panel.main(['-y ' + str(image.ProfileData.Profiles[options.profile][1][1]), '-i', '-m', path], qtGUI)
|
||||
if options.imgproc:
|
||||
print("\nProcessing images...")
|
||||
if GUI:
|
||||
|
||||
178
kcc/image.py
178
kcc/image.py
@@ -98,7 +98,7 @@ class ProfileData:
|
||||
|
||||
|
||||
class ComicPage:
|
||||
def __init__(self, source, options, fill=None):
|
||||
def __init__(self, source, options, original=None):
|
||||
try:
|
||||
self.profile_label, self.size, self.palette, self.gamma, self.panelviewsize = options.profileData
|
||||
except KeyError:
|
||||
@@ -107,58 +107,67 @@ class ComicPage:
|
||||
self.filename = os.path.basename(self.origFileName)
|
||||
self.image = Image.open(source)
|
||||
self.image = self.image.convert('RGB')
|
||||
self.rotated = None
|
||||
self.border = None
|
||||
self.noHPV = None
|
||||
self.noVPV = None
|
||||
self.noPV = None
|
||||
self.purge = False
|
||||
self.hq = False
|
||||
self.opt = options
|
||||
if fill:
|
||||
self.fill = fill
|
||||
if original:
|
||||
self.second = True
|
||||
self.rotated = original.rotated
|
||||
self.border = original.border
|
||||
self.noHPV = original.noHPV
|
||||
self.noVPV = original.noVPV
|
||||
self.noPV = original.noPV
|
||||
self.noHQ = original.noHQ
|
||||
self.fill = original.fill
|
||||
self.color = original.color
|
||||
if self.rotated:
|
||||
self.image = self.image.rotate(90, Image.BICUBIC, True)
|
||||
self.opt.quality = 0
|
||||
else:
|
||||
self.second = False
|
||||
self.rotated = None
|
||||
self.border = None
|
||||
self.noHPV = None
|
||||
self.noVPV = None
|
||||
self.noPV = None
|
||||
self.fill = None
|
||||
if options.webtoon:
|
||||
self.color = True
|
||||
else:
|
||||
self.color = self.isImageColor()
|
||||
self.noHQ = False
|
||||
if options.webtoon:
|
||||
self.color = True
|
||||
else:
|
||||
self.color = self.isImageColor()
|
||||
|
||||
def saveToDir(self, targetdir):
|
||||
try:
|
||||
if not self.purge:
|
||||
flags = []
|
||||
filename = os.path.join(targetdir, os.path.splitext(self.filename)[0]) + '-KCC'
|
||||
if not self.opt.forcecolor and not self.opt.forcepng:
|
||||
self.image = self.image.convert('L')
|
||||
if self.rotated:
|
||||
flags.append('Rotated')
|
||||
if self.hq:
|
||||
flags.append('HighQuality')
|
||||
filename += '-HQ'
|
||||
if self.noPV:
|
||||
flags.append('NoPanelView')
|
||||
else:
|
||||
if self.noHPV:
|
||||
flags.append('NoHorizontalPanelView')
|
||||
if self.noVPV:
|
||||
flags.append('NoVerticalPanelView')
|
||||
if self.border:
|
||||
flags.append('Margins-' + str(self.border[0]) + '-' + str(self.border[1]) + '-'
|
||||
+ str(self.border[2]) + '-' + str(self.border[3]))
|
||||
if self.opt.forcepng:
|
||||
filename += '.png'
|
||||
self.image.save(filename, 'PNG', optimize=1)
|
||||
else:
|
||||
filename += '.jpg'
|
||||
self.image.save(filename, 'JPEG', optimize=1, quality=80)
|
||||
return [md5Checksum(filename), flags]
|
||||
flags = []
|
||||
filename = os.path.join(targetdir, os.path.splitext(self.filename)[0]) + '-KCC'
|
||||
if not self.opt.forcecolor and not self.opt.forcepng:
|
||||
self.image = self.image.convert('L')
|
||||
if self.rotated:
|
||||
flags.append('Rotated')
|
||||
if self.noPV:
|
||||
flags.append('NoPanelView')
|
||||
else:
|
||||
return None
|
||||
if self.noHPV:
|
||||
flags.append('NoHorizontalPanelView')
|
||||
if self.noVPV:
|
||||
flags.append('NoVerticalPanelView')
|
||||
if self.border:
|
||||
flags.append('Margins-' + str(self.border[0]) + '-' + str(self.border[1]) + '-'
|
||||
+ str(self.border[2]) + '-' + str(self.border[3]))
|
||||
if self.fill != 'white':
|
||||
flags.append('BlackFill')
|
||||
if self.opt.quality == 2:
|
||||
filename += '-HQ'
|
||||
if self.opt.forcepng:
|
||||
filename += '.png'
|
||||
self.image.save(filename, 'PNG', optimize=1)
|
||||
else:
|
||||
filename += '.jpg'
|
||||
self.image.save(filename, 'JPEG', optimize=1, quality=80)
|
||||
return [md5Checksum(filename), flags]
|
||||
except IOError as e:
|
||||
raise RuntimeError('Cannot write image in directory %s: %s' % (targetdir, e))
|
||||
|
||||
def optimizeImage(self):
|
||||
def autocontrastImage(self):
|
||||
gamma = self.opt.gamma
|
||||
if gamma < 0.1:
|
||||
gamma = self.gamma
|
||||
@@ -180,80 +189,65 @@ class ComicPage:
|
||||
# Quantize is deprecated but new function call it internally anyway...
|
||||
self.image = self.image.quantize(palette=palImg)
|
||||
|
||||
def calculateBorderPercent(self, x, img, isWidth):
|
||||
if isWidth:
|
||||
return int(round(float(x)/float(img.image.size[0]), 4) * 10000 * 1.5)
|
||||
else:
|
||||
return int(round(float(x)/float(img.image.size[1]), 4) * 10000 * 1.5)
|
||||
|
||||
def calculateBorder(self, sourceImage, isHQ=False):
|
||||
if (isHQ and sourceImage.purge) or self.noPV:
|
||||
self.border = [0, 0, 0, 0]
|
||||
self.noPV = True
|
||||
def calculateBorder(self):
|
||||
if self.noPV:
|
||||
self.border = [0.0, 0.0, 0.0, 0.0]
|
||||
return
|
||||
if self.fill == 'white':
|
||||
# Only already saved files can have P mode. So we can break color quantization.
|
||||
if sourceImage.image.mode == 'P':
|
||||
sourceImage.image = sourceImage.image.convert('RGB')
|
||||
border = ImageChops.invert(sourceImage.image).getbbox()
|
||||
border = ImageChops.invert(self.image).getbbox()
|
||||
else:
|
||||
border = sourceImage.image.getbbox()
|
||||
border = self.image.getbbox()
|
||||
if self.opt.quality == 2:
|
||||
multiplier = 1.0
|
||||
else:
|
||||
multiplier = 1.5
|
||||
if border is not None:
|
||||
if isHQ:
|
||||
multiplier = 1.0
|
||||
else:
|
||||
multiplier = 1.5
|
||||
self.border = [self.calculateBorderPercent(border[0], sourceImage, True),
|
||||
self.calculateBorderPercent(border[1], sourceImage, False),
|
||||
self.calculateBorderPercent((sourceImage.image.size[0] - border[2]), sourceImage, True),
|
||||
self.calculateBorderPercent((sourceImage.image.size[1] - border[3]), sourceImage, False)]
|
||||
if int((border[2] - border[0]) * multiplier) < self.size[0]:
|
||||
self.border = [round(float(border[0])/float(self.image.size[0])*150, 3),
|
||||
round(float(border[1])/float(self.image.size[1])*150, 3),
|
||||
round(float(self.image.size[0]-border[2])/float(self.image.size[0])*150, 3),
|
||||
round(float(self.image.size[1]-border[3])/float(self.image.size[1])*150, 3)]
|
||||
if int((border[2] - border[0]) * multiplier) < self.size[0] + 10:
|
||||
self.noHPV = True
|
||||
if int((border[3] - border[1]) * multiplier) < self.size[1]:
|
||||
if int((border[3] - border[1]) * multiplier) < self.size[1] + 10:
|
||||
self.noVPV = True
|
||||
else:
|
||||
self.border = [0, 0, 0, 0]
|
||||
self.border = [0.0, 0.0, 0.0, 0.0]
|
||||
self.noHPV = True
|
||||
self.noVPV = True
|
||||
|
||||
def resizeImage(self, qualityMode=None):
|
||||
upscale = self.opt.upscale
|
||||
stretch = self.opt.stretch
|
||||
bordersColor = self.opt.bordersColor
|
||||
if qualityMode is None:
|
||||
qualityMode = self.opt.quality
|
||||
if bordersColor:
|
||||
fill = bordersColor
|
||||
def resizeImage(self):
|
||||
if self.opt.bordersColor:
|
||||
fill = self.opt.bordersColor
|
||||
else:
|
||||
fill = self.fill
|
||||
# Set target size
|
||||
if qualityMode == 0:
|
||||
if self.opt.quality == 0:
|
||||
size = (self.size[0], self.size[1])
|
||||
elif qualityMode == 1 and not stretch and not upscale and self.image.size[0] <=\
|
||||
elif self.opt.quality == 1 and not self.opt.stretch and not self.opt.upscale and self.image.size[0] <=\
|
||||
self.size[0] and self.image.size[1] <= self.size[1]:
|
||||
size = (self.size[0], self.size[1])
|
||||
elif qualityMode == 1:
|
||||
elif self.opt.quality == 1:
|
||||
# Forcing upscale to make sure that margins will be not too big
|
||||
if not stretch:
|
||||
upscale = True
|
||||
if not self.opt.stretch:
|
||||
self.opt.upscale = True
|
||||
size = (self.panelviewsize[0], self.panelviewsize[1])
|
||||
elif qualityMode == 2 and not stretch and not upscale and self.image.size[0] <=\
|
||||
elif self.opt.quality == 2 and not self.opt.stretch and not self.opt.upscale and self.image.size[0] <=\
|
||||
self.size[0] and self.image.size[1] <= self.size[1]:
|
||||
self.purge = True
|
||||
return self.image
|
||||
# HQ version will not be needed
|
||||
self.noHQ = True
|
||||
return
|
||||
else:
|
||||
self.hq = True
|
||||
size = (self.panelviewsize[0], self.panelviewsize[1])
|
||||
# If stretching is on - Resize without other considerations
|
||||
if stretch:
|
||||
if self.opt.stretch:
|
||||
if self.image.size[0] <= size[0] and self.image.size[1] <= size[1]:
|
||||
method = Image.BICUBIC
|
||||
else:
|
||||
method = Image.LANCZOS
|
||||
self.image = self.image.resize(size, method)
|
||||
return self.image
|
||||
return
|
||||
# If image is smaller than target resolution and upscale is off - Just expand it by adding margins
|
||||
if self.image.size[0] <= size[0] and self.image.size[1] <= size[1] and not upscale:
|
||||
if self.image.size[0] <= size[0] and self.image.size[1] <= size[1] and not self.opt.upscale:
|
||||
borderw = int((size[0] - self.image.size[0]) / 2)
|
||||
borderh = int((size[1] - self.image.size[1]) / 2)
|
||||
# PV is disabled when source image is smaller than device screen and upscale is off
|
||||
@@ -263,7 +257,7 @@ class ComicPage:
|
||||
# Border can't be float so sometimes image might be 1px too small/large
|
||||
if self.image.size[0] != size[0] or self.image.size[1] != size[1]:
|
||||
self.image = ImageOps.fit(self.image, size, method=Image.BICUBIC, centering=(0.5, 0.5))
|
||||
return self.image
|
||||
return
|
||||
# Otherwise - Upscale/Downscale
|
||||
ratioDev = float(size[0]) / float(size[1])
|
||||
if (float(self.image.size[0]) / float(self.image.size[1])) < ratioDev:
|
||||
@@ -277,7 +271,7 @@ class ComicPage:
|
||||
else:
|
||||
method = Image.LANCZOS
|
||||
self.image = ImageOps.fit(self.image, size, method=method, centering=(0.5, 0.5))
|
||||
return self.image
|
||||
return
|
||||
|
||||
def splitPage(self, targetdir):
|
||||
width, height = self.image.size
|
||||
@@ -370,7 +364,6 @@ class ComicPage:
|
||||
else:
|
||||
diff = pageNumberCut1
|
||||
self.image = self.image.crop((0, 0, widthImg, heightImg - diff))
|
||||
return self.image
|
||||
|
||||
def cropWhiteSpace(self):
|
||||
if ImageChops.invert(self.image).getbbox() is not None:
|
||||
@@ -406,7 +399,6 @@ class ComicPage:
|
||||
diff += delta
|
||||
diff -= delta
|
||||
self.image = self.image.crop((0, 0, widthImg - diff, heightImg))
|
||||
return self.image
|
||||
|
||||
def getImageHistogram(self, image):
|
||||
histogram = image.histogram()
|
||||
|
||||
@@ -69,7 +69,7 @@ class MetadataParser:
|
||||
stdout=PIPE, stderr=STDOUT, shell=True)
|
||||
extracted = False
|
||||
for line in output.stdout:
|
||||
if b"Everything is Ok" in line:
|
||||
if b"Everything is Ok" in line or b"No files to process" in line:
|
||||
extracted = True
|
||||
if not extracted:
|
||||
rmtree(workdir)
|
||||
|
||||
@@ -20,11 +20,14 @@ import os
|
||||
from hashlib import md5
|
||||
from html.parser import HTMLParser
|
||||
from distutils.version import StrictVersion
|
||||
from scandir import walk
|
||||
from time import sleep
|
||||
from shutil import rmtree, move
|
||||
from tempfile import mkdtemp
|
||||
from zipfile import ZipFile, ZIP_DEFLATED
|
||||
try:
|
||||
from scandir import walk
|
||||
except ImportError:
|
||||
walk = None
|
||||
|
||||
|
||||
class HTMLStripper(HTMLParser):
|
||||
|
||||
Reference in New Issue
Block a user