Compare commits
No commits in common. "9816ee7f6d6062db04975ebcad13ab28e58ae97e" and "91be974dbcab512905fcfa193346b1c123d6edb1" have entirely different histories.
9816ee7f6d
...
91be974dbc
File diff suppressed because it is too large
Load Diff
9
Makefile
9
Makefile
|
@ -1,8 +1,7 @@
|
||||||
build:
|
build:
|
||||||
@mkdir -p out
|
python3 build.py
|
||||||
@python3 build.py -B 2> out/err.log
|
py3clean .
|
||||||
@py3clean .
|
|
||||||
|
|
||||||
install: build
|
install: build
|
||||||
@cp out/KawaiiMonoRegularPatched.ttf ~/.local/share/fonts/KawaiiMonoRegular.ttf
|
cp out/KawaiiMonoRegularPatched.ttf ~/.local/share/fonts/KawaiiMonoRegular.ttf
|
||||||
@fc-cache -f -v
|
fc-cache -f -v
|
||||||
|
|
4
build.py
4
build.py
|
@ -1,7 +1,3 @@
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
sys.path.append(os.path.realpath(os.path.dirname(__file__)+"/src"))
|
|
||||||
sys.path.append(os.path.realpath(os.path.dirname(__file__)+"/assets"))
|
|
||||||
from src.build import build
|
from src.build import build
|
||||||
import config
|
import config
|
||||||
if __name__ == "__main__": build(config.config)
|
if __name__ == "__main__": build(config.config)
|
||||||
|
|
18
config.py
18
config.py
|
@ -41,31 +41,25 @@ config = {
|
||||||
# 폰트 용량이 매우 커집니다!!
|
# 폰트 용량이 매우 커집니다!!
|
||||||
"CopyKoreanGlyphs": True,
|
"CopyKoreanGlyphs": True,
|
||||||
|
|
||||||
|
# TODO: 이 설정은 아직 동작하지 않습니다
|
||||||
# 노토 산스 모노에서 가타카나/히라가나
|
# 노토 산스 모노에서 가타카나/히라가나
|
||||||
# 글리프를 복사할 지에 대한 여부입니다
|
# 글리프를 복사할 지에 대한 여부입니다
|
||||||
# 한자는 포함하지 않습니다
|
# 한자는 포함하지 않습니다
|
||||||
"CopyJapaneseGlyphs": True,
|
"CopyJapaneseGlyphs": False,
|
||||||
|
|
||||||
# 노토 산스에서 단위관련 기호, 원형 기호
|
|
||||||
# 글리프를 복사할 지에 대한 여부입니다
|
|
||||||
# 폰트위 용량이 매우 커집니다!!
|
|
||||||
"CopySymbols": True,
|
|
||||||
|
|
||||||
|
# TODO: 이 설정은 아직 동작하지 않습니다
|
||||||
# 노토 산스 모노에서 CJK 공용 한자 글리프를
|
# 노토 산스 모노에서 CJK 공용 한자 글리프를
|
||||||
# 복사할 지에 대한 여부입니다.
|
# 복사할 지에 대한 여부입니다.
|
||||||
# 폰트 용량이 매우 커집니다!!
|
# 폰트 용량이 매우 커집니다!!
|
||||||
# 웹용 폰트의 경우 끄는것을 추천합니다
|
# 웹용 폰트의 경우 끄는것을 추천합니다
|
||||||
#! 최소 2분 이상 걸립니다!!
|
"CopyCJKUnifiedIdeographs": False,
|
||||||
"CopyCJKUnifiedIdeographs": True, # 일반적인 CJK Unified
|
|
||||||
"CopyCJKUnifiedIdeographsExtension": False, # Extension A~F
|
|
||||||
"CopyCJKCompatibilityIdeographs": False, # Compatibility Ideograph, Supplement
|
|
||||||
#! 이 옵션들을 활성화시 ttf 포멧으로 저장에 실패할 수 있습니다
|
|
||||||
#? ttf 의 글자수 제한 때문에 그런것이므로, 다른 포멧으로 저장해야합니다.
|
|
||||||
|
|
||||||
|
# TODO: 이 설정은 아직 동작하지 않습니다
|
||||||
# 라틴 글리프를 Hack 폰트에서 더 가져옵니다
|
# 라틴 글리프를 Hack 폰트에서 더 가져옵니다
|
||||||
# (성조 표시된 라틴, ...)
|
# (성조 표시된 라틴, ...)
|
||||||
"CopyLatinExtra": True,
|
"CopyLatinExtra": True,
|
||||||
|
|
||||||
|
# TODO: 이 설정은 아직 동작하지 않습니다
|
||||||
# Nerd Fonts 패치를 적용할지에 대한
|
# Nerd Fonts 패치를 적용할지에 대한
|
||||||
# 여부입니다. 폰트 용량이 매우 커집니다!!
|
# 여부입니다. 폰트 용량이 매우 커집니다!!
|
||||||
# 웹용 폰트의 경우 끄는것을 추천합니다
|
# 웹용 폰트의 경우 끄는것을 추천합니다
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
from . import wgetHandler
|
||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
import shutil
|
||||||
|
import math
|
||||||
|
from . import utility as Utility
|
||||||
|
import fontforge
|
||||||
|
|
||||||
|
link_NanumSquareNeo = "https://campaign.naver.com/nanumsquare_neo/download/NaverNanumSquareNeo.zip"
|
||||||
|
patchVersion = 2 # 업데이트 후 캐시를 무시하기 위해서 사용
|
||||||
|
|
||||||
|
# 폰트 다운로드와 열기
|
||||||
|
# 배포 방식이 zip 으로 배포이기 때문에 zipfile 라이브러리로
|
||||||
|
# 다운로드 후 언팩함
|
||||||
|
def getFontPath():
|
||||||
|
if not os.path.exists("assets"): os.mkdir("assets")
|
||||||
|
if not os.path.exists("assets/NanumSquareNeoKr.ttf"):
|
||||||
|
if not os.path.exists("assets/NanumSquareNeoKr.zip"):
|
||||||
|
wgetHandler.download(link_NanumSquareNeo,"assets/NanumSquareNeoKr.zip")
|
||||||
|
print("Unzipping NanumSquareNeoKr.zip",end="")
|
||||||
|
with zipfile.ZipFile("assets/NanumSquareNeoKr.zip", 'r') as zip_ref:
|
||||||
|
extractName = zip_ref.extract("NaverNanumSquareNeo/TTF/NanumSquareNeo-bRg.ttf","assets/NanumSquareNeoKr.extract")
|
||||||
|
os.rename(extractName,"assets/NanumSquareNeoKr.ttf")
|
||||||
|
shutil.rmtree('assets/NanumSquareNeoKr.extract')
|
||||||
|
print(" [OK]")
|
||||||
|
return "assets/NanumSquareNeoKr.ttf"
|
||||||
|
|
||||||
|
# 한글 범위의 글립을 선택함
|
||||||
|
def selectGlyphs(font):
|
||||||
|
font.selection.none()
|
||||||
|
font.selection.select(("more","ranges","unicode"),0x3131,0x32BF) # ㄱ ~ ㊿
|
||||||
|
font.selection.select(("more","ranges","unicode"),0xAC00,0xD7A3) # 가 ~ 힣
|
||||||
|
|
||||||
|
# 굵기/폭 설정 캐시파일 만들기
|
||||||
|
def getCache(sourcePath,baseSize=550,weight=16):
|
||||||
|
# 캐시된 파일을 확인하고 있으면 반환
|
||||||
|
filename = "assets/cache/NanumSquareNeoKr.cache_{}.base_{}.weight_{}.sfd".format(patchVersion,baseSize,weight)
|
||||||
|
if os.path.exists(filename):
|
||||||
|
return fontforge.open(filename)
|
||||||
|
|
||||||
|
# 새로운 캐시용 폰트 생성
|
||||||
|
cache=fontforge.font()
|
||||||
|
cache.encoding = 'UnicodeFull'
|
||||||
|
|
||||||
|
# 소스 폰트를 패치시킴
|
||||||
|
source=fontforge.open(sourcePath)
|
||||||
|
selectGlyphs(source)
|
||||||
|
source.changeWeight(weight) # 굵기 변경
|
||||||
|
|
||||||
|
# 너비 지정
|
||||||
|
Utility.setWidthWithSavingPosition(
|
||||||
|
font=source,targetWidth=baseSize*2
|
||||||
|
)
|
||||||
|
|
||||||
|
# 캐시에 붇여넣기
|
||||||
|
source.copy()
|
||||||
|
selectGlyphs(cache)
|
||||||
|
cache.paste()
|
||||||
|
|
||||||
|
# 캐시 폰트 저장
|
||||||
|
if not os.path.exists("assets/cache"): os.mkdir("assets/cache")
|
||||||
|
cache.save(filename)
|
||||||
|
return cache
|
||||||
|
|
||||||
|
# Regular 같은 문자열 weight 를 포인트 값으로 변경
|
||||||
|
weightStrToNum = {
|
||||||
|
"Regular": 16,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 캐시를 가져와서 글리프를 타겟 폰트에 붇여넣음
|
||||||
|
def pasteGlyphs(target,sourcePath,baseSize=550,weightStr="Regular"):
|
||||||
|
|
||||||
|
# 캐시된 소스를 읽어드림
|
||||||
|
source = getCache(
|
||||||
|
sourcePath = sourcePath,
|
||||||
|
baseSize = baseSize,
|
||||||
|
weight = weightStrToNum.get(weightStr)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 타겟으로 글리프 복사
|
||||||
|
selectGlyphs(source)
|
||||||
|
source.copy()
|
||||||
|
selectGlyphs(target)
|
||||||
|
target.paste()
|
||||||
|
|
||||||
|
# 캐시 닫기
|
||||||
|
source.close()
|
|
@ -0,0 +1,38 @@
|
||||||
|
from . import wgetHandler
|
||||||
|
import os
|
||||||
|
|
||||||
|
github_NotoSansMonoCJKkr = "https://github.com/googlefonts/noto-cjk/raw/main/Sans/Mono/NotoSansMonoCJKkr-Regular.otf"
|
||||||
|
|
||||||
|
# 폰트 다운로드와 열기
|
||||||
|
def getFontPath():
|
||||||
|
if not os.path.exists("assets"): os.mkdir("assets")
|
||||||
|
if not os.path.exists("assets/NotoMonoCJKkr.otf"):
|
||||||
|
wgetHandler.download(github_NotoSansMonoCJKkr,"assets/NotoMonoCJKkr.otf")
|
||||||
|
return "assets/NotoMonoCJKkr.otf"
|
||||||
|
|
||||||
|
def pasteGlyphs(target,source,baseSize=550,JapaneseGlyphs=False,CJKUnifiedIdeographs=False):
|
||||||
|
source.cidFlatten()
|
||||||
|
|
||||||
|
def select(font):
|
||||||
|
font.selection.none()
|
||||||
|
font.selection.select(("more","ranges","unicode"),0x3131,0x32BF) # ㄱ ~ ㊿
|
||||||
|
font.selection.select(("more","ranges","unicode"),0xAC00,0xD7A3) # 가 ~ 힣
|
||||||
|
select(source)
|
||||||
|
|
||||||
|
# 넓은 글자 크기 (한글에 모두 적용)
|
||||||
|
wideWidth = baseSize*2
|
||||||
|
for glyph in source.selection.byGlyphs:
|
||||||
|
widthDiff = wideWidth-glyph.width # 타겟 너비와 얼마나 크기 차이가 나는지
|
||||||
|
sideAdjust = widthDiff/2 # 좌우 사이드 조정해야하는 정도
|
||||||
|
glyph.left_side_bearing = int(glyph.left_side_bearing + math.floor(sideAdjust)) # 좌우 베어링을 조정함
|
||||||
|
glyph.right_side_bearing = int(glyph.right_side_bearing + math.ceil(sideAdjust))
|
||||||
|
glyph.width = wideWidth # 타겟 너비로 정확하게 설정
|
||||||
|
|
||||||
|
# 타겟으로 글리프 복사
|
||||||
|
source.copy()
|
||||||
|
select(target)
|
||||||
|
target.paste()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
getFontPath()
|
|
@ -0,0 +1,58 @@
|
||||||
|
import fontforge
|
||||||
|
import os
|
||||||
|
|
||||||
|
from . import NanumSquareNeo as NanumSquareNeoLoader
|
||||||
|
from . import NotoMono as NotoMonoLoader
|
||||||
|
from . import KawaiiMono as KawaiiMonoLoader
|
||||||
|
from . import utility as Utility
|
||||||
|
|
||||||
|
def build(config=None):
|
||||||
|
# 메인 폰트 불러오기
|
||||||
|
kawaii = fontforge.open(
|
||||||
|
KawaiiMonoLoader.getFontPath())
|
||||||
|
|
||||||
|
# 모든 글리프를 붇여넣을 수 있도록 인코딩을 utf full 로 변경
|
||||||
|
kawaii.encoding = 'UnicodeFull'
|
||||||
|
|
||||||
|
# 폰트 가로폭 설정
|
||||||
|
baseSize = config.get("FontBaseWidth")
|
||||||
|
if baseSize != 550:
|
||||||
|
Utility.setWidthWithSavingPosition(
|
||||||
|
font=kawaii,targetWidth=baseSize
|
||||||
|
)
|
||||||
|
|
||||||
|
# 한글 글리프 붇여넣기
|
||||||
|
if config.get("CopyKoreanGlyphs"):
|
||||||
|
# 나눔 스퀘어 네오 다운로드/불러오기
|
||||||
|
nanumSquareNeo = NanumSquareNeoLoader.getFontPath()
|
||||||
|
# 글리프 붇여넣기
|
||||||
|
NanumSquareNeoLoader.pasteGlyphs(
|
||||||
|
target=kawaii,baseSize=baseSize,weightStr="Regular",
|
||||||
|
sourcePath=nanumSquareNeo)
|
||||||
|
|
||||||
|
if (config.get("CopyJapaneseGlyphs") or
|
||||||
|
config.get("CopyCJKUnifiedIdeographs")):
|
||||||
|
# 노토 모노 다운로드/불러오기
|
||||||
|
notoMono = fontforge.open(
|
||||||
|
NotoMonoLoader.getFontPath())
|
||||||
|
# 글리프 붇여넣기
|
||||||
|
NotoMonoLoader.pasteGlyphs(
|
||||||
|
JapaneseGlyphs=config.get("CopyJapaneseGlyphs") or False,
|
||||||
|
CJKUnifiedIdeographs=config.get("CopyCJKUnifiedIdeographs") or False,
|
||||||
|
target=kawaii,baseSize=550,
|
||||||
|
source=notoMono)
|
||||||
|
notoMono.close()
|
||||||
|
|
||||||
|
# 생성
|
||||||
|
if not os.path.exists("out"): os.mkdir("out")
|
||||||
|
kawaii.generate("out/"+"KawaiiMonoRegularPatched.ttf")
|
||||||
|
|
||||||
|
# KawaiiMonoRegularPatched.ttf
|
||||||
|
# KawaiiMonoRegularPatched.otf
|
||||||
|
# KawaiiMonoRegularPatched.woff
|
||||||
|
# KawaiiMonoRegularPatched.eot
|
||||||
|
|
||||||
|
# 파일 닫기
|
||||||
|
kawaii.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__": build()
|
|
@ -1,7 +0,0 @@
|
||||||
|
|
||||||
patchVersion = 2 # 업데이트 후 캐시를 무시하기 위해서 사용
|
|
||||||
|
|
||||||
from . import select as selectGlyphs
|
|
||||||
from .download import download
|
|
||||||
from .cacheBuilder import getCachedFont
|
|
||||||
from .patcher import pasteGlyphs
|
|
|
@ -1,40 +0,0 @@
|
||||||
|
|
||||||
import utility as Utility
|
|
||||||
import fontforge
|
|
||||||
import os
|
|
||||||
from . import selectGlyphs
|
|
||||||
from . import patchVersion
|
|
||||||
|
|
||||||
# 굵기/폭 설정 캐시파일 만들기
|
|
||||||
def getCachedFont(sourcePath,baseSize=550,weight=16):
|
|
||||||
# 캐시된 파일을 확인하고 있으면 반환
|
|
||||||
filename = "assets/cache/NanumSquareNeoKr.cache_{}.base_{}.weight_{}.sfd".format(patchVersion,baseSize,weight)
|
|
||||||
if os.path.exists(filename):
|
|
||||||
print("Found build cache [OK]")
|
|
||||||
return fontforge.open(filename)
|
|
||||||
print("Creating new build cache",end="")
|
|
||||||
|
|
||||||
# 새로운 캐시용 폰트 생성
|
|
||||||
cache=fontforge.font()
|
|
||||||
cache.encoding = 'UnicodeFull'
|
|
||||||
|
|
||||||
# 소스 폰트를 패치시킴
|
|
||||||
source=fontforge.open(sourcePath)
|
|
||||||
selectGlyphs.Korean(source)
|
|
||||||
source.changeWeight(weight) # 굵기 변경
|
|
||||||
|
|
||||||
# 너비 지정
|
|
||||||
Utility.setWidthWithSavingPosition(
|
|
||||||
font=source,targetWidth=baseSize*2
|
|
||||||
)
|
|
||||||
|
|
||||||
# 캐시에 붇여넣기
|
|
||||||
source.copy()
|
|
||||||
selectGlyphs.Korean(cache)
|
|
||||||
cache.paste()
|
|
||||||
|
|
||||||
# 캐시 폰트 저장
|
|
||||||
if not os.path.exists("assets/cache"): os.mkdir("assets/cache")
|
|
||||||
cache.save(filename)
|
|
||||||
print(" [OK]")
|
|
||||||
return cache
|
|
|
@ -1,22 +0,0 @@
|
||||||
import zipfile
|
|
||||||
import shutil
|
|
||||||
import wgetHandler
|
|
||||||
import os
|
|
||||||
|
|
||||||
link_NanumSquareNeo = "https://campaign.naver.com/nanumsquare_neo/download/NaverNanumSquareNeo.zip"
|
|
||||||
|
|
||||||
# 폰트 다운로드와 열기
|
|
||||||
# 배포 방식이 zip 으로 배포이기 때문에 zipfile 라이브러리로
|
|
||||||
# 다운로드 후 언팩함
|
|
||||||
def download():
|
|
||||||
if not os.path.exists("assets"): os.mkdir("assets")
|
|
||||||
if not os.path.exists("assets/NanumSquareNeoKr.ttf"):
|
|
||||||
if not os.path.exists("assets/NanumSquareNeoKr.zip"):
|
|
||||||
wgetHandler.download(link_NanumSquareNeo,"assets/NanumSquareNeoKr.zip")
|
|
||||||
print("Unzipping NanumSquareNeoKr.zip",end="")
|
|
||||||
with zipfile.ZipFile("assets/NanumSquareNeoKr.zip", 'r') as zip_ref:
|
|
||||||
extractName = zip_ref.extract("NaverNanumSquareNeo/TTF/NanumSquareNeo-bRg.ttf","assets/NanumSquareNeoKr.extract")
|
|
||||||
os.rename(extractName,"assets/NanumSquareNeoKr.ttf")
|
|
||||||
shutil.rmtree('assets/NanumSquareNeoKr.extract')
|
|
||||||
print(" [OK]")
|
|
||||||
return "assets/NanumSquareNeoKr.ttf"
|
|
|
@ -1,32 +0,0 @@
|
||||||
from . import getCachedFont
|
|
||||||
from . import selectGlyphs
|
|
||||||
|
|
||||||
# Regular 같은 문자열 weight 를 포인트 값으로 변경
|
|
||||||
weightStrToNum = {
|
|
||||||
"Regular": 16,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 캐시를 가져와서 글리프를 타겟 폰트에 붇여넣음
|
|
||||||
def pasteGlyphs(target,sourcePath,deselectOriginalGlyphs,baseSize=550,weightStr="Regular"):
|
|
||||||
|
|
||||||
# 캐시된 소스를 읽어드림
|
|
||||||
print("Patching: NanumSquareNeo")
|
|
||||||
source = getCachedFont(
|
|
||||||
sourcePath = sourcePath,
|
|
||||||
baseSize = baseSize,
|
|
||||||
weight = weightStrToNum.get(weightStr)
|
|
||||||
)
|
|
||||||
|
|
||||||
selectGlyphs.Clear(source)
|
|
||||||
selectGlyphs.Clear(target)
|
|
||||||
|
|
||||||
# 타겟으로 글리프 복사
|
|
||||||
selectGlyphs.Korean(source)
|
|
||||||
deselectOriginalGlyphs(source)
|
|
||||||
source.copy()
|
|
||||||
selectGlyphs.Korean(target)
|
|
||||||
deselectOriginalGlyphs(target)
|
|
||||||
target.paste()
|
|
||||||
|
|
||||||
# 캐시 닫기
|
|
||||||
source.close()
|
|
|
@ -1,8 +0,0 @@
|
||||||
# 한글 범위의 글립을 선택함
|
|
||||||
def Korean(font):
|
|
||||||
font.selection.none()
|
|
||||||
font.selection.select(("more","ranges","unicode"),0x3131,0x32BF) # ㄱ ~ ㊿
|
|
||||||
font.selection.select(("more","ranges","unicode"),0xAC00,0xD7A3) # 가 ~ 힣
|
|
||||||
|
|
||||||
def Clear(font):
|
|
||||||
font.selection.none()
|
|
|
@ -1,3 +0,0 @@
|
||||||
|
|
||||||
from .download import downloadPatcher
|
|
||||||
from .download import nerdFonts_Download_Test
|
|
|
@ -1,7 +0,0 @@
|
||||||
|
|
||||||
patchVersion = 2
|
|
||||||
|
|
||||||
from .download import download
|
|
||||||
from . import select as selectGlyphs
|
|
||||||
from .cacheBuilder import getCachedFont
|
|
||||||
from .patcher import pasteGlyphs
|
|
|
@ -1,56 +0,0 @@
|
||||||
|
|
||||||
import utility as Utility
|
|
||||||
import fontforge
|
|
||||||
import os
|
|
||||||
from . import selectGlyphs
|
|
||||||
from . import patchVersion
|
|
||||||
|
|
||||||
# 굵기/폭 설정 캐시파일 만들기
|
|
||||||
def getCachedFont(sourcePath,EnabledItems,baseSize=550,weight=16):
|
|
||||||
# 캐시된 파일을 확인하고 있으면 반환
|
|
||||||
filename = "assets/cache/NotoMono.cache_{}.base_{}.weight_{}{}{}{}{}{}.sfd".format(
|
|
||||||
patchVersion,baseSize,weight,
|
|
||||||
EnabledItems.get("JapaneseGlyphs") and ".jp" or "",
|
|
||||||
EnabledItems.get("CJKUnifiedIdeographs") and ".id" or "",
|
|
||||||
EnabledItems.get("CopyCJKUnifiedIdeographsExtension") and ".ide" or "",
|
|
||||||
EnabledItems.get("CopyCJKCompatibilityIdeographs") and ".cid" or "",
|
|
||||||
EnabledItems.get("Symbols") and ".sym" or "")
|
|
||||||
if os.path.exists(filename):
|
|
||||||
print("Found build cache [OK]")
|
|
||||||
return fontforge.open(filename)
|
|
||||||
print("Creating new build cache",end="")
|
|
||||||
|
|
||||||
# 새로운 캐시용 폰트 생성
|
|
||||||
cache=fontforge.font()
|
|
||||||
cache.encoding = 'UnicodeFull'
|
|
||||||
|
|
||||||
# 소스 폰트를 패치시킴
|
|
||||||
source=fontforge.open(sourcePath)
|
|
||||||
source.cidFlatten()
|
|
||||||
source.encoding = 'UnicodeFull'
|
|
||||||
if EnabledItems.get("JapaneseGlyphs"): selectGlyphs.JapaneseGlyphs(source)
|
|
||||||
if EnabledItems.get("Symbols"): selectGlyphs.Symbols(source)
|
|
||||||
source.changeWeight(weight) # 굵기 변경
|
|
||||||
selectGlyphs.SelectByEnabledList(source,EnabledItems)
|
|
||||||
# 한자 글립은 냥많아서 크기조절하면 끝이 안난다냥
|
|
||||||
|
|
||||||
# 너비 지정
|
|
||||||
Utility.setWidthWithSavingPosition(
|
|
||||||
font=source,targetWidth=baseSize*2
|
|
||||||
)
|
|
||||||
|
|
||||||
Utility.scale(font=source,targetScale=0.85)
|
|
||||||
|
|
||||||
# 캐시에 붇여넣기
|
|
||||||
source.copy()
|
|
||||||
selectGlyphs.SelectByEnabledList(cache,EnabledItems)
|
|
||||||
cache.paste()
|
|
||||||
|
|
||||||
selectGlyphs.Clear(cache)
|
|
||||||
selectGlyphs.Clear(source)
|
|
||||||
|
|
||||||
# 캐시 폰트 저장
|
|
||||||
if not os.path.exists("assets/cache"): os.mkdir("assets/cache")
|
|
||||||
cache.save(filename)
|
|
||||||
print(" [OK]")
|
|
||||||
return cache
|
|
|
@ -1,11 +0,0 @@
|
||||||
import os
|
|
||||||
import wgetHandler
|
|
||||||
|
|
||||||
github_NotoSansMonoCJKkr = "https://github.com/googlefonts/noto-cjk/raw/main/Sans/Mono/NotoSansMonoCJKkr-Regular.otf"
|
|
||||||
|
|
||||||
# 폰트 다운로드와 열기
|
|
||||||
def download():
|
|
||||||
if not os.path.exists("assets"): os.mkdir("assets")
|
|
||||||
if not os.path.exists("assets/NotoMonoCJKkr.otf"):
|
|
||||||
wgetHandler.download(github_NotoSansMonoCJKkr,"assets/NotoMonoCJKkr.otf")
|
|
||||||
return "assets/NotoMonoCJKkr.otf"
|
|
|
@ -1,30 +0,0 @@
|
||||||
from . import selectGlyphs
|
|
||||||
from . import getCachedFont
|
|
||||||
|
|
||||||
# Regular 같은 문자열 weight 를 포인트 값으로 변경
|
|
||||||
weightStrToNum = {
|
|
||||||
"Regular": 16,
|
|
||||||
}
|
|
||||||
|
|
||||||
def pasteGlyphs(target,sourcePath,deselectOriginalGlyphs,EnabledItems,baseSize=550,weightStr="Regular"):
|
|
||||||
# 캐시된 소스를 읽어드림
|
|
||||||
source = getCachedFont(
|
|
||||||
sourcePath = sourcePath,
|
|
||||||
baseSize = baseSize,
|
|
||||||
weight = weightStrToNum.get(weightStr),
|
|
||||||
EnabledItems = EnabledItems
|
|
||||||
)
|
|
||||||
|
|
||||||
selectGlyphs.Clear(source)
|
|
||||||
selectGlyphs.Clear(target)
|
|
||||||
|
|
||||||
# 타겟으로 글리프 복사
|
|
||||||
selectGlyphs.SelectByEnabledList(source,EnabledItems)
|
|
||||||
deselectOriginalGlyphs(source)
|
|
||||||
source.copy()
|
|
||||||
selectGlyphs.SelectByEnabledList(source,target)
|
|
||||||
deselectOriginalGlyphs(target)
|
|
||||||
target.paste()
|
|
||||||
|
|
||||||
# 캐시 닫기
|
|
||||||
source.close()
|
|
|
@ -1,73 +0,0 @@
|
||||||
selFlag = ("more","ranges","unicode")
|
|
||||||
|
|
||||||
# 일본어 글립 선택
|
|
||||||
def JapaneseGlyphs(font):
|
|
||||||
# 사각문자 (Square)
|
|
||||||
font.selection.select(selFlag,0x32FF,0x2271)
|
|
||||||
font.selection.select(selFlag,0x337B,0x337F) # Missing one char
|
|
||||||
font.selection.select(selFlag,0x1F200,0x1F200)
|
|
||||||
|
|
||||||
# 히라가나/가타카나
|
|
||||||
font.selection.select(selFlag,0x3041,0x30FF)
|
|
||||||
font.selection.select(selFlag,0x31F0,0x31FF) # SMALL Katakana
|
|
||||||
font.selection.select(selFlag,0xFF66,0xFF9F) # HalfWidth Katakana
|
|
||||||
|
|
||||||
# 일어 기호
|
|
||||||
font.selection.select(selFlag,0xFF5B,0xFF65)
|
|
||||||
|
|
||||||
# CJK 한자 글립 선택
|
|
||||||
def CJKUnifiedIdeographs(font):
|
|
||||||
# CJK Stroke
|
|
||||||
font.selection.select(selFlag,0x31C0,0x31E3)
|
|
||||||
|
|
||||||
# Bopomofo
|
|
||||||
font.selection.select(selFlag,0x3105,0x312F)
|
|
||||||
font.selection.select(selFlag,0x31A0,0x31BB)
|
|
||||||
|
|
||||||
# CJK Unified Ideograph
|
|
||||||
font.selection.select(selFlag,0x4E00,0x9FFF)
|
|
||||||
|
|
||||||
|
|
||||||
def CJKUnifiedIdeographsExtension(font):
|
|
||||||
font.selection.select(selFlag,0x3400,0x4DBF) # Extension A
|
|
||||||
font.selection.select(selFlag,0x20000,0x2A6DF) # Extension B
|
|
||||||
font.selection.select(selFlag,0x2A700,0x2B73F) # Extension C
|
|
||||||
font.selection.select(selFlag,0x2B740,0x2B81F) # Extension D
|
|
||||||
font.selection.select(selFlag,0x2B820,0x2CEAF) # Extension E
|
|
||||||
font.selection.select(selFlag,0x2CEB0,0x2EBEF) # Extension F
|
|
||||||
|
|
||||||
def CJKCompatibilityIdeographs(font):
|
|
||||||
# CJK Compatibility Ideograph
|
|
||||||
font.selection.select(selFlag,0xF900,0xFAFF)
|
|
||||||
# CJK Compatibility Ideographs Supplement
|
|
||||||
font.selection.select(selFlag,0x2F800,0x2FA1F)
|
|
||||||
|
|
||||||
def Symbols(font):
|
|
||||||
# 원형, 단위기호
|
|
||||||
font.selection.select(selFlag,0x3220,0x3250)
|
|
||||||
font.selection.select(selFlag,0x3220,0x3250)
|
|
||||||
font.selection.select(selFlag,0x3280,0x32B0) # Missing ...
|
|
||||||
font.selection.select(selFlag,0x32C0,0x32FE)
|
|
||||||
|
|
||||||
# 단위 기호
|
|
||||||
font.selection.select(selFlag,0x3358,0x337A)
|
|
||||||
font.selection.select(selFlag,0x3380,0x33FF)
|
|
||||||
|
|
||||||
# Latin Ligature
|
|
||||||
font.selection.select(selFlag,0xFB00,0xFB04)
|
|
||||||
|
|
||||||
# 특수기호/FULLWIDTH Latin
|
|
||||||
font.selection.select(selFlag,0xFE10,0xFF5A)
|
|
||||||
font.selection.select(selFlag,0xFFE0,0xFFEE)
|
|
||||||
font.selection.select(selFlag,0x1F100,0x1F1AC)
|
|
||||||
font.selection.select(selFlag,0x1F201,0x1F251)
|
|
||||||
|
|
||||||
def SelectByEnabledList(target,EnabledItems):
|
|
||||||
if EnabledItems.get("JapaneseGlyphs"): JapaneseGlyphs(target)
|
|
||||||
if EnabledItems.get("CJKUnifiedIdeographs"): CJKUnifiedIdeographs(target)
|
|
||||||
if EnabledItems.get("CJKUnifiedIdeographsExtension"): CJKUnifiedIdeographsExtension(target)
|
|
||||||
if EnabledItems.get("CJKCompatibilityIdeographs"): CJKCompatibilityIdeographs(target)
|
|
||||||
if EnabledItems.get("Symbols"): Symbols(target)
|
|
||||||
|
|
||||||
def Clear(font):
|
|
||||||
font.selection.none()
|
|
|
@ -1,4 +0,0 @@
|
||||||
|
|
||||||
from .build import build
|
|
||||||
|
|
||||||
from .NerdFonts import nerdFonts_Download_Test
|
|
|
@ -1,91 +0,0 @@
|
||||||
import os
|
|
||||||
import utility as Utility
|
|
||||||
import fontforge
|
|
||||||
|
|
||||||
from . import NerdFonts as NerdFontsLoader
|
|
||||||
from . import NanumSquareNeo as NanumSquareNeoLoader
|
|
||||||
from . import NotoMono as NotoMonoLoader
|
|
||||||
from . import KawaiiMono as KawaiiMonoLoader
|
|
||||||
|
|
||||||
deselectFlags = ("less","unicode")
|
|
||||||
|
|
||||||
def build(config=None):
|
|
||||||
# 메인 폰트 불러오기 / 에셋 다운로드
|
|
||||||
kawaii = fontforge.open(
|
|
||||||
KawaiiMonoLoader.getFontPath())
|
|
||||||
print("-------------- DOWNLOAD PATCH CONTENTS --------------")
|
|
||||||
# 나눔 스퀘어 네오 다운로드
|
|
||||||
nanumSquareNeo = None
|
|
||||||
if config.get("CopyKoreanGlyphs"):
|
|
||||||
nanumSquareNeo = NanumSquareNeoLoader.download()
|
|
||||||
# 노토 모노 다운로드/불러오기
|
|
||||||
notoMono = None
|
|
||||||
if (config.get("CopyJapaneseGlyphs") or
|
|
||||||
config.get("CopyCJKUnifiedIdeographs") or
|
|
||||||
config.get("CopyCJKUnifiedIdeographsExtension") or
|
|
||||||
config.get("CopyCJKCompatibilityIdeographs")):
|
|
||||||
notoMono = NotoMonoLoader.download()
|
|
||||||
# Nerd fonts 패치기 다운로드
|
|
||||||
nerdFontsPatcherPath = None
|
|
||||||
if config.get("NerdFonts"):
|
|
||||||
nerdFontsPatcherPath = NerdFontsLoader.downloadPatcher()
|
|
||||||
print("--------------------- Patching ---------------------")
|
|
||||||
|
|
||||||
# 유지 목록 (덮어쓰기 금지) 만들기
|
|
||||||
kawaii.selection.all()
|
|
||||||
keepList = [i.unicode for i in kawaii.selection.byGlyphs]
|
|
||||||
def deselectOriginalGlyphs(target):
|
|
||||||
for unicode in keepList:
|
|
||||||
if unicode == -1: continue
|
|
||||||
target.selection.select(deselectFlags,unicode)
|
|
||||||
|
|
||||||
# 모든 글리프를 붇여넣을 수 있도록 인코딩을 utf full 로 변경
|
|
||||||
kawaii.encoding = 'UnicodeFull'
|
|
||||||
|
|
||||||
# 폰트 가로폭 설정
|
|
||||||
baseSize = config.get("FontBaseWidth")
|
|
||||||
if baseSize != 550:
|
|
||||||
Utility.setWidthWithSavingPosition(
|
|
||||||
font=kawaii,targetWidth=baseSize
|
|
||||||
)
|
|
||||||
|
|
||||||
# 한글 글리프 붇여넣기
|
|
||||||
if nanumSquareNeo:
|
|
||||||
# 글리프 붇여넣기
|
|
||||||
NanumSquareNeoLoader.pasteGlyphs(
|
|
||||||
target=kawaii,baseSize=baseSize,weightStr="Regular",
|
|
||||||
sourcePath=nanumSquareNeo,
|
|
||||||
deselectOriginalGlyphs = deselectOriginalGlyphs)
|
|
||||||
|
|
||||||
# 일어 글리프 혹은 한자 글리프 추가
|
|
||||||
if notoMono:
|
|
||||||
# 글리프 붇여넣기
|
|
||||||
NotoMonoLoader.pasteGlyphs(
|
|
||||||
EnabledItems = {
|
|
||||||
"JapaneseGlyphs": config.get("CopyJapaneseGlyphs") or False,
|
|
||||||
"CJKUnifiedIdeographs": config.get("CopyCJKUnifiedIdeographs") or False,
|
|
||||||
"CJKUnifiedIdeographsExtension": config.get("CopyCJKUnifiedIdeographsExtension"),
|
|
||||||
"CJKCompatibilityIdeographs": config.get("CopyCJKCompatibilityIdeographs"),
|
|
||||||
},
|
|
||||||
Symbols=config.get("CopySymbols") or False,
|
|
||||||
target=kawaii,baseSize=550,
|
|
||||||
sourcePath=notoMono,
|
|
||||||
deselectOriginalGlyphs = deselectOriginalGlyphs)
|
|
||||||
|
|
||||||
# NerdFonts 패치 적용
|
|
||||||
# if config.get("NerdFonts"):
|
|
||||||
# nerdFontsPatcherPath = NerdFontsLoader.downloadPatcher()
|
|
||||||
|
|
||||||
# 생성
|
|
||||||
if not os.path.exists("out"): os.mkdir("out")
|
|
||||||
kawaii.generate("out/"+"KawaiiMonoRegularPatched.ttf")
|
|
||||||
|
|
||||||
# KawaiiMonoRegularPatched.ttf
|
|
||||||
# KawaiiMonoRegularPatched.otf
|
|
||||||
# KawaiiMonoRegularPatched.woff
|
|
||||||
# KawaiiMonoRegularPatched.eot
|
|
||||||
|
|
||||||
# 파일 닫기
|
|
||||||
kawaii.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__": build()
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
def pasteGlyphs(target,sourceList):
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,20 @@
|
||||||
import os
|
import os
|
||||||
import wgetHandler
|
|
||||||
import zipfile
|
import zipfile
|
||||||
|
# from .. import wgetHandler
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib
|
||||||
|
basePath = os.path.realpath(os.path.dirname(__file__)+"/../")
|
||||||
|
wgetHandler = importlib.util.module_from_spec(importlib.util.spec_from_file_location("wgetHandler",basePath+"/wgetHandler.py"))
|
||||||
|
# import wgetHandler
|
||||||
|
|
||||||
|
print("__file__ : {__file__}".format(__file__=__file__))
|
||||||
|
print("wgetHandler (spec)")
|
||||||
|
print(importlib.util.spec_from_file_location("wgetHandler",basePath))
|
||||||
|
print("wgetHandler [object]")
|
||||||
|
print(wgetHandler)
|
||||||
|
print(help(wgetHandler))
|
||||||
|
# import wgetHandler
|
||||||
|
|
||||||
link_FontPatcher = "https://github.com/ryanoasis/nerd-fonts/releases/latest/download/FontPatcher.zip"
|
link_FontPatcher = "https://github.com/ryanoasis/nerd-fonts/releases/latest/download/FontPatcher.zip"
|
||||||
|
|
||||||
|
@ -12,10 +26,7 @@ def downloadPatcher():
|
||||||
print("Unzipping NerdFontPatcher.zip",end="")
|
print("Unzipping NerdFontPatcher.zip",end="")
|
||||||
with zipfile.ZipFile("assets/NerdFontPatcher.zip", 'r') as zip_ref:
|
with zipfile.ZipFile("assets/NerdFontPatcher.zip", 'r') as zip_ref:
|
||||||
extractName = zip_ref.extractall("assets/NerdFontPatcher_extract")
|
extractName = zip_ref.extractall("assets/NerdFontPatcher_extract")
|
||||||
os.rename("assets/NerdFontPatcher_extract/font-patcher","assets/NerdFontPatcher_extract/fontPatcher.py")
|
|
||||||
with open("assets/NerdFontPatcher_extract/__init__.py","w") as file:
|
|
||||||
file.write("")
|
|
||||||
print(" [OK]")
|
print(" [OK]")
|
||||||
return "assets/NerdFontPatcher_extract"
|
return "assets/NanumSquareNeoKr.ttf"
|
||||||
|
|
||||||
def nerdFonts_Download_Test(): downloadPatcher()
|
if __name__ == "__main__": downloadPatcher()
|
|
@ -1,5 +1,4 @@
|
||||||
import math
|
import math
|
||||||
import psMat
|
|
||||||
|
|
||||||
def setWidthWithSavingPosition(font,targetWidth):
|
def setWidthWithSavingPosition(font,targetWidth):
|
||||||
for glyph in font.selection.byGlyphs:
|
for glyph in font.selection.byGlyphs:
|
||||||
|
@ -8,7 +7,3 @@ def setWidthWithSavingPosition(font,targetWidth):
|
||||||
glyph.left_side_bearing = int(glyph.left_side_bearing + math.floor(sideAdjust)) # 좌우 베어링을 조정함
|
glyph.left_side_bearing = int(glyph.left_side_bearing + math.floor(sideAdjust)) # 좌우 베어링을 조정함
|
||||||
glyph.right_side_bearing = int(glyph.right_side_bearing + math.ceil(sideAdjust))
|
glyph.right_side_bearing = int(glyph.right_side_bearing + math.ceil(sideAdjust))
|
||||||
glyph.width = targetWidth # 타겟 너비로 정확하게 설정
|
glyph.width = targetWidth # 타겟 너비로 정확하게 설정
|
||||||
|
|
||||||
def scale(font,targetScale):
|
|
||||||
font.transform(psMat.scale(targetScale))
|
|
||||||
font.round()
|
|
|
@ -1,2 +0,0 @@
|
||||||
|
|
||||||
from .utility import *
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit fdd3a0f8404ccab90f939f9952af139e6c55142a
|
|
@ -1,6 +1,5 @@
|
||||||
import math
|
|
||||||
|
|
||||||
from .wget import wget
|
from .wget import wget
|
||||||
|
import math
|
||||||
|
|
||||||
# bar 를 커스텀.... 하는 어떤 글에서 가져온거
|
# bar 를 커스텀.... 하는 어떤 글에서 가져온거
|
||||||
def bar_custom(current, total, width=80):
|
def bar_custom(current, total, width=80):
|
|
@ -1,2 +0,0 @@
|
||||||
|
|
||||||
from .wgetHandler import *
|
|
|
@ -1,95 +0,0 @@
|
||||||
Usage
|
|
||||||
=====
|
|
||||||
|
|
||||||
python -m wget [options] <URL>
|
|
||||||
|
|
||||||
options:
|
|
||||||
-o --output FILE|DIR output filename or directory
|
|
||||||
|
|
||||||
|
|
||||||
API Usage
|
|
||||||
=========
|
|
||||||
|
|
||||||
>>> import wget
|
|
||||||
>>> url = 'http://www.futurecrew.com/skaven/song_files/mp3/razorback.mp3'
|
|
||||||
>>> filename = wget.download(url)
|
|
||||||
100% [................................................] 3841532 / 3841532>
|
|
||||||
>> filename
|
|
||||||
'razorback.mp3'
|
|
||||||
|
|
||||||
The skew that you see above is a documented side effect.
|
|
||||||
Alternative progress bar:
|
|
||||||
|
|
||||||
>>> wget.download(url, bar=bar_thermometer)
|
|
||||||
|
|
||||||
|
|
||||||
ChangeLog
|
|
||||||
=========
|
|
||||||
2.2 (2014-07-19)
|
|
||||||
* it again can download without -o option
|
|
||||||
|
|
||||||
2.1 (2014-07-10)
|
|
||||||
* it shows command line help
|
|
||||||
* -o option allows to select output file/directory
|
|
||||||
|
|
||||||
* download(url, out, bar) contains out parameter
|
|
||||||
|
|
||||||
2.0 (2013-04-26)
|
|
||||||
* it shows percentage
|
|
||||||
* it has usage examples
|
|
||||||
* it changes if being used as a library
|
|
||||||
|
|
||||||
* download shows progress bar by default
|
|
||||||
* bar_adaptive gets improved algorithm
|
|
||||||
* download(url, bar) contains bar parameter
|
|
||||||
* bar(current, total)
|
|
||||||
* progress_callback is named callback_progress
|
|
||||||
|
|
||||||
1.0 (2012-11-13)
|
|
||||||
* it runs with Python 3
|
|
||||||
|
|
||||||
0.9 (2012-11-13)
|
|
||||||
* it renames file if it already exists
|
|
||||||
* it can be used as a library
|
|
||||||
|
|
||||||
* download(url) returns filename
|
|
||||||
* bar_adaptive() draws progress bar
|
|
||||||
* bar_thermometer() simplified bar
|
|
||||||
|
|
||||||
0.8 (2011-05-03)
|
|
||||||
* it detects filename from HTTP headers
|
|
||||||
|
|
||||||
0.7 (2011-03-01)
|
|
||||||
* compatibility fix for Python 2.5
|
|
||||||
* limit width of progress bar to 100 chars
|
|
||||||
|
|
||||||
0.6 (2010-04-24)
|
|
||||||
* it detects console width on POSIX
|
|
||||||
|
|
||||||
0.5 (2010-04-23)
|
|
||||||
* it detects console width on Windows
|
|
||||||
|
|
||||||
0.4 (2010-04-15)
|
|
||||||
* it shows cute progress bar
|
|
||||||
|
|
||||||
0.3 (2010-04-05)
|
|
||||||
* it creates temp file in current dir
|
|
||||||
|
|
||||||
0.2 (2010-02-16)
|
|
||||||
* it tries to detect filename from URL
|
|
||||||
|
|
||||||
0.1 (2010-02-04)
|
|
||||||
* it can download file
|
|
||||||
|
|
||||||
|
|
||||||
Release Checklist
|
|
||||||
=================
|
|
||||||
|
|
||||||
| [ ] update version in wget.py
|
|
||||||
| [x] update description in setup.py
|
|
||||||
| [ ] python setup.py check -mrs
|
|
||||||
| [ ] python setup.py sdist upload
|
|
||||||
| [ ] tag hg version
|
|
||||||
|
|
||||||
--
|
|
||||||
anatoly techtonik <techtonik@gmail.com>
|
|
|
@ -1,37 +0,0 @@
|
||||||
from distutils.core import setup
|
|
||||||
|
|
||||||
|
|
||||||
def get_version(relpath):
|
|
||||||
"""read version info from file without importing it"""
|
|
||||||
from os.path import dirname, join
|
|
||||||
for line in open(join(dirname(__file__), relpath)):
|
|
||||||
if '__version__' in line:
|
|
||||||
if '"' in line:
|
|
||||||
# __version__ = "0.9"
|
|
||||||
return line.split('"')[1]
|
|
||||||
elif "'" in line:
|
|
||||||
return line.split("'")[1]
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name='wget',
|
|
||||||
version=get_version('wget.py'),
|
|
||||||
author='anatoly techtonik <techtonik@gmail.com>',
|
|
||||||
url='http://bitbucket.org/techtonik/python-wget/',
|
|
||||||
|
|
||||||
description="pure python download utility",
|
|
||||||
license="Public Domain",
|
|
||||||
classifiers=[
|
|
||||||
'Environment :: Console',
|
|
||||||
'License :: Public Domain',
|
|
||||||
'Operating System :: OS Independent',
|
|
||||||
'Programming Language :: Python :: 2',
|
|
||||||
'Programming Language :: Python :: 3',
|
|
||||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
|
||||||
'Topic :: System :: Networking',
|
|
||||||
'Topic :: Utilities',
|
|
||||||
],
|
|
||||||
|
|
||||||
py_modules=['wget'],
|
|
||||||
|
|
||||||
long_description=open('README.txt').read(),
|
|
||||||
)
|
|
|
@ -1,402 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
"""
|
|
||||||
Download utility as an easy way to get file from the net
|
|
||||||
|
|
||||||
python -m wget <URL>
|
|
||||||
python wget.py <URL>
|
|
||||||
|
|
||||||
Downloads: http://pypi.python.org/pypi/wget/
|
|
||||||
Development: http://bitbucket.org/techtonik/python-wget/
|
|
||||||
|
|
||||||
wget.py is not option compatible with Unix wget utility,
|
|
||||||
to make command line interface intuitive for new people.
|
|
||||||
|
|
||||||
Public domain by anatoly techtonik <techtonik@gmail.com>
|
|
||||||
Also available under the terms of MIT license
|
|
||||||
Copyright (c) 2010-2014 anatoly techtonik
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
import sys, shutil, os
|
|
||||||
import tempfile
|
|
||||||
import math
|
|
||||||
|
|
||||||
PY3K = sys.version_info >= (3, 0)
|
|
||||||
if PY3K:
|
|
||||||
import urllib.request as urllib
|
|
||||||
import urllib.parse as urlparse
|
|
||||||
else:
|
|
||||||
import urllib
|
|
||||||
import urlparse
|
|
||||||
|
|
||||||
|
|
||||||
__version__ = "2.3-beta1"
|
|
||||||
|
|
||||||
|
|
||||||
def filename_from_url(url):
|
|
||||||
""":return: detected filename or None"""
|
|
||||||
fname = os.path.basename(urlparse.urlparse(url).path)
|
|
||||||
if len(fname.strip(" \n\t.")) == 0:
|
|
||||||
return None
|
|
||||||
return fname
|
|
||||||
|
|
||||||
def filename_from_headers(headers):
|
|
||||||
"""Detect filename from Content-Disposition headers if present.
|
|
||||||
http://greenbytes.de/tech/tc2231/
|
|
||||||
|
|
||||||
:param: headers as dict, list or string
|
|
||||||
:return: filename from content-disposition header or None
|
|
||||||
"""
|
|
||||||
if type(headers) == str:
|
|
||||||
headers = headers.splitlines()
|
|
||||||
if type(headers) == list:
|
|
||||||
headers = dict([x.split(':', 1) for x in headers])
|
|
||||||
cdisp = headers.get("Content-Disposition")
|
|
||||||
if not cdisp:
|
|
||||||
return None
|
|
||||||
cdtype = cdisp.split(';')
|
|
||||||
if len(cdtype) == 1:
|
|
||||||
return None
|
|
||||||
if cdtype[0].strip().lower() not in ('inline', 'attachment'):
|
|
||||||
return None
|
|
||||||
# several filename params is illegal, but just in case
|
|
||||||
fnames = [x for x in cdtype[1:] if x.strip().startswith('filename=')]
|
|
||||||
if len(fnames) > 1:
|
|
||||||
return None
|
|
||||||
name = fnames[0].split('=')[1].strip(' \t"')
|
|
||||||
name = os.path.basename(name)
|
|
||||||
if not name:
|
|
||||||
return None
|
|
||||||
return name
|
|
||||||
|
|
||||||
def filename_fix_existing(filename):
|
|
||||||
"""Expands name portion of filename with numeric ' (x)' suffix to
|
|
||||||
return filename that doesn't exist already.
|
|
||||||
"""
|
|
||||||
dirname = '.'
|
|
||||||
name, ext = filename.rsplit('.', 1)
|
|
||||||
names = [x for x in os.listdir(dirname) if x.startswith(name)]
|
|
||||||
names = [x.rsplit('.', 1)[0] for x in names]
|
|
||||||
suffixes = [x.replace(name, '') for x in names]
|
|
||||||
# filter suffixes that match ' (x)' pattern
|
|
||||||
suffixes = [x[2:-1] for x in suffixes
|
|
||||||
if x.startswith(' (') and x.endswith(')')]
|
|
||||||
indexes = [int(x) for x in suffixes
|
|
||||||
if set(x) <= set('0123456789')]
|
|
||||||
idx = 1
|
|
||||||
if indexes:
|
|
||||||
idx += sorted(indexes)[-1]
|
|
||||||
return '%s (%d).%s' % (name, idx, ext)
|
|
||||||
|
|
||||||
|
|
||||||
# --- terminal/console output helpers ---
|
|
||||||
|
|
||||||
def get_console_width():
|
|
||||||
"""Return width of available window area. Autodetection works for
|
|
||||||
Windows and POSIX platforms. Returns 80 for others
|
|
||||||
|
|
||||||
Code from http://bitbucket.org/techtonik/python-pager
|
|
||||||
"""
|
|
||||||
|
|
||||||
if os.name == 'nt':
|
|
||||||
STD_INPUT_HANDLE = -10
|
|
||||||
STD_OUTPUT_HANDLE = -11
|
|
||||||
STD_ERROR_HANDLE = -12
|
|
||||||
|
|
||||||
# get console handle
|
|
||||||
from ctypes import windll, Structure, byref
|
|
||||||
try:
|
|
||||||
from ctypes.wintypes import SHORT, WORD, DWORD
|
|
||||||
except ImportError:
|
|
||||||
# workaround for missing types in Python 2.5
|
|
||||||
from ctypes import (
|
|
||||||
c_short as SHORT, c_ushort as WORD, c_ulong as DWORD)
|
|
||||||
console_handle = windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
|
|
||||||
|
|
||||||
# CONSOLE_SCREEN_BUFFER_INFO Structure
|
|
||||||
class COORD(Structure):
|
|
||||||
_fields_ = [("X", SHORT), ("Y", SHORT)]
|
|
||||||
|
|
||||||
class SMALL_RECT(Structure):
|
|
||||||
_fields_ = [("Left", SHORT), ("Top", SHORT),
|
|
||||||
("Right", SHORT), ("Bottom", SHORT)]
|
|
||||||
|
|
||||||
class CONSOLE_SCREEN_BUFFER_INFO(Structure):
|
|
||||||
_fields_ = [("dwSize", COORD),
|
|
||||||
("dwCursorPosition", COORD),
|
|
||||||
("wAttributes", WORD),
|
|
||||||
("srWindow", SMALL_RECT),
|
|
||||||
("dwMaximumWindowSize", DWORD)]
|
|
||||||
|
|
||||||
sbi = CONSOLE_SCREEN_BUFFER_INFO()
|
|
||||||
ret = windll.kernel32.GetConsoleScreenBufferInfo(console_handle, byref(sbi))
|
|
||||||
if ret == 0:
|
|
||||||
return 0
|
|
||||||
return sbi.srWindow.Right+1
|
|
||||||
|
|
||||||
elif os.name == 'posix':
|
|
||||||
from fcntl import ioctl
|
|
||||||
from termios import TIOCGWINSZ
|
|
||||||
from array import array
|
|
||||||
|
|
||||||
winsize = array("H", [0] * 4)
|
|
||||||
try:
|
|
||||||
ioctl(sys.stdout.fileno(), TIOCGWINSZ, winsize)
|
|
||||||
except IOError:
|
|
||||||
pass
|
|
||||||
return (winsize[1], winsize[0])[0]
|
|
||||||
|
|
||||||
return 80
|
|
||||||
|
|
||||||
|
|
||||||
def bar_thermometer(current, total, width=80):
|
|
||||||
"""Return thermometer style progress bar string. `total` argument
|
|
||||||
can not be zero. The minimum size of bar returned is 3. Example:
|
|
||||||
|
|
||||||
[.......... ]
|
|
||||||
|
|
||||||
Control and trailing symbols (\r and spaces) are not included.
|
|
||||||
See `bar_adaptive` for more information.
|
|
||||||
"""
|
|
||||||
# number of dots on thermometer scale
|
|
||||||
avail_dots = width-2
|
|
||||||
shaded_dots = int(math.floor(float(current) / total * avail_dots))
|
|
||||||
return '[' + '.'*shaded_dots + ' '*(avail_dots-shaded_dots) + ']'
|
|
||||||
|
|
||||||
def bar_adaptive(current, total, width=80):
|
|
||||||
"""Return progress bar string for given values in one of three
|
|
||||||
styles depending on available width:
|
|
||||||
|
|
||||||
[.. ] downloaded / total
|
|
||||||
downloaded / total
|
|
||||||
[.. ]
|
|
||||||
|
|
||||||
if total value is unknown or <= 0, show bytes counter using two
|
|
||||||
adaptive styles:
|
|
||||||
|
|
||||||
%s / unknown
|
|
||||||
%s
|
|
||||||
|
|
||||||
if there is not enough space on the screen, do not display anything
|
|
||||||
|
|
||||||
returned string doesn't include control characters like \r used to
|
|
||||||
place cursor at the beginning of the line to erase previous content.
|
|
||||||
|
|
||||||
this function leaves one free character at the end of string to
|
|
||||||
avoid automatic linefeed on Windows.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# process special case when total size is unknown and return immediately
|
|
||||||
if not total or total < 0:
|
|
||||||
msg = "%s / unknown" % current
|
|
||||||
if len(msg) < width: # leaves one character to avoid linefeed
|
|
||||||
return msg
|
|
||||||
if len("%s" % current) < width:
|
|
||||||
return "%s" % current
|
|
||||||
|
|
||||||
# --- adaptive layout algorithm ---
|
|
||||||
#
|
|
||||||
# [x] describe the format of the progress bar
|
|
||||||
# [x] describe min width for each data field
|
|
||||||
# [x] set priorities for each element
|
|
||||||
# [x] select elements to be shown
|
|
||||||
# [x] choose top priority element min_width < avail_width
|
|
||||||
# [x] lessen avail_width by value if min_width
|
|
||||||
# [x] exclude element from priority list and repeat
|
|
||||||
|
|
||||||
# 10% [.. ] 10/100
|
|
||||||
# pppp bbbbb sssssss
|
|
||||||
|
|
||||||
min_width = {
|
|
||||||
'percent': 4, # 100%
|
|
||||||
'bar': 3, # [.]
|
|
||||||
'size': len("%s" % total)*2 + 3, # 'xxxx / yyyy'
|
|
||||||
}
|
|
||||||
priority = ['percent', 'bar', 'size']
|
|
||||||
|
|
||||||
# select elements to show
|
|
||||||
selected = []
|
|
||||||
avail = width
|
|
||||||
for field in priority:
|
|
||||||
if min_width[field] < avail:
|
|
||||||
selected.append(field)
|
|
||||||
avail -= min_width[field]+1 # +1 is for separator or for reserved space at
|
|
||||||
# the end of line to avoid linefeed on Windows
|
|
||||||
# render
|
|
||||||
output = ''
|
|
||||||
for field in selected:
|
|
||||||
|
|
||||||
if field == 'percent':
|
|
||||||
# fixed size width for percentage
|
|
||||||
output += ('%s%%' % (100 * current // total)).rjust(min_width['percent'])
|
|
||||||
elif field == 'bar': # [. ]
|
|
||||||
# bar takes its min width + all available space
|
|
||||||
output += bar_thermometer(current, total, min_width['bar']+avail)
|
|
||||||
elif field == 'size':
|
|
||||||
# size field has a constant width (min == max)
|
|
||||||
output += ("%s / %s" % (current, total)).rjust(min_width['size'])
|
|
||||||
|
|
||||||
selected = selected[1:]
|
|
||||||
if selected:
|
|
||||||
output += ' ' # add field separator
|
|
||||||
|
|
||||||
return output
|
|
||||||
|
|
||||||
# --/ console helpers
|
|
||||||
|
|
||||||
|
|
||||||
__current_size = 0 # global state variable, which exists solely as a
|
|
||||||
# workaround against Python 3.3.0 regression
|
|
||||||
# http://bugs.python.org/issue16409
|
|
||||||
# fixed in Python 3.3.1
|
|
||||||
def callback_progress(blocks, block_size, total_size, bar_function):
|
|
||||||
"""callback function for urlretrieve that is called when connection is
|
|
||||||
created and when once for each block
|
|
||||||
|
|
||||||
draws adaptive progress bar in terminal/console
|
|
||||||
|
|
||||||
use sys.stdout.write() instead of "print,", because it allows one more
|
|
||||||
symbol at the line end without linefeed on Windows
|
|
||||||
|
|
||||||
:param blocks: number of blocks transferred so far
|
|
||||||
:param block_size: in bytes
|
|
||||||
:param total_size: in bytes, can be -1 if server doesn't return it
|
|
||||||
:param bar_function: another callback function to visualize progress
|
|
||||||
"""
|
|
||||||
global __current_size
|
|
||||||
|
|
||||||
width = min(100, get_console_width())
|
|
||||||
|
|
||||||
if sys.version_info[:3] == (3, 3, 0): # regression workaround
|
|
||||||
if blocks == 0: # first call
|
|
||||||
__current_size = 0
|
|
||||||
else:
|
|
||||||
__current_size += block_size
|
|
||||||
current_size = __current_size
|
|
||||||
else:
|
|
||||||
current_size = min(blocks*block_size, total_size)
|
|
||||||
progress = bar_function(current_size, total_size, width)
|
|
||||||
if progress:
|
|
||||||
sys.stdout.write("\r" + progress)
|
|
||||||
|
|
||||||
class ThrowOnErrorOpener(urllib.FancyURLopener):
|
|
||||||
def http_error_default(self, url, fp, errcode, errmsg, headers):
|
|
||||||
raise Exception("%s: %s" % (errcode, errmsg))
|
|
||||||
|
|
||||||
def download(url, out=None, bar=bar_adaptive):
|
|
||||||
"""High level function, which downloads URL into tmp file in current
|
|
||||||
directory and then renames it to filename autodetected from either URL
|
|
||||||
or HTTP headers.
|
|
||||||
|
|
||||||
:param bar: function to track download progress (visualize etc.)
|
|
||||||
:param out: output filename or directory
|
|
||||||
:return: filename where URL is downloaded to
|
|
||||||
"""
|
|
||||||
names = dict()
|
|
||||||
names["out"] = out or ''
|
|
||||||
names["url"] = filename_from_url(url)
|
|
||||||
# get filename for temp file in current directory
|
|
||||||
prefix = (names["url"] or names["out"] or ".") + "."
|
|
||||||
(fd, tmpfile) = tempfile.mkstemp(".tmp", prefix=prefix, dir=".")
|
|
||||||
os.close(fd)
|
|
||||||
os.unlink(tmpfile)
|
|
||||||
|
|
||||||
# set progress monitoring callback
|
|
||||||
def callback_charged(blocks, block_size, total_size):
|
|
||||||
# 'closure' to set bar drawing function in callback
|
|
||||||
callback_progress(blocks, block_size, total_size, bar_function=bar)
|
|
||||||
if bar:
|
|
||||||
callback = callback_charged
|
|
||||||
else:
|
|
||||||
callback = None
|
|
||||||
|
|
||||||
(tmpfile, headers) = ThrowOnErrorOpener().retrieve(url, tmpfile, callback)
|
|
||||||
names["header"] = filename_from_headers(headers)
|
|
||||||
if os.path.isdir(names["out"]):
|
|
||||||
filename = names["header"] or names["url"]
|
|
||||||
filename = names["out"] + "/" + filename
|
|
||||||
else:
|
|
||||||
filename = names["out"] or names["header"] or names["url"]
|
|
||||||
# add numeric ' (x)' suffix if filename already exists
|
|
||||||
if os.path.exists(filename):
|
|
||||||
filename = filename_fix_existing(filename)
|
|
||||||
shutil.move(tmpfile, filename)
|
|
||||||
|
|
||||||
#print headers
|
|
||||||
return filename
|
|
||||||
|
|
||||||
|
|
||||||
usage = """\
|
|
||||||
usage: wget.py [options] URL
|
|
||||||
|
|
||||||
options:
|
|
||||||
-o --output FILE|DIR output filename or directory
|
|
||||||
-h --help
|
|
||||||
--version
|
|
||||||
"""
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
if len(sys.argv) < 2 or "-h" in sys.argv or "--help" in sys.argv:
|
|
||||||
sys.exit(usage)
|
|
||||||
if "--version" in sys.argv:
|
|
||||||
sys.exit("wget.py " + __version__)
|
|
||||||
|
|
||||||
from optparse import OptionParser
|
|
||||||
parser = OptionParser()
|
|
||||||
parser.add_option("-o", "--output", dest="output")
|
|
||||||
(options, args) = parser.parse_args()
|
|
||||||
|
|
||||||
url = sys.argv[1]
|
|
||||||
filename = download(args[0], out=options.output)
|
|
||||||
|
|
||||||
print("")
|
|
||||||
print("Saved under %s" % filename)
|
|
||||||
|
|
||||||
r"""
|
|
||||||
features that require more tuits for urlretrieve API
|
|
||||||
http://www.python.org/doc/2.6/library/urllib.html#urllib.urlretrieve
|
|
||||||
|
|
||||||
[x] autodetect filename from URL
|
|
||||||
[x] autodetect filename from headers - Content-Disposition
|
|
||||||
http://greenbytes.de/tech/tc2231/
|
|
||||||
[ ] make HEAD request to detect temp filename from Content-Disposition
|
|
||||||
[ ] process HTTP status codes (i.e. 404 error)
|
|
||||||
http://ftp.de.debian.org/debian/pool/iso-codes_3.24.2.orig.tar.bz2
|
|
||||||
[ ] catch KeyboardInterrupt
|
|
||||||
[ ] optionally preserve incomplete file
|
|
||||||
[x] create temp file in current directory
|
|
||||||
[ ] resume download (broken connection)
|
|
||||||
[ ] resume download (incomplete file)
|
|
||||||
[x] show progress indicator
|
|
||||||
http://mail.python.org/pipermail/tutor/2005-May/038797.html
|
|
||||||
[x] do not overwrite downloaded file
|
|
||||||
[x] rename file automatically if exists
|
|
||||||
[x] optionally specify path for downloaded file
|
|
||||||
|
|
||||||
[ ] options plan
|
|
||||||
[x] -h, --help, --version (CHAOS speccy)
|
|
||||||
[ ] clpbar progress bar style
|
|
||||||
_ 30.0Mb at 3.0 Mbps eta: 0:00:20 30% [===== ]
|
|
||||||
[ ] test "bar \r" print with \r at the end of line on Windows
|
|
||||||
[ ] process Python 2.x urllib.ContentTooShortError exception gracefully
|
|
||||||
(ideally retry and continue download)
|
|
||||||
|
|
||||||
(tmpfile, headers) = urllib.urlretrieve(url, tmpfile, callback_progress)
|
|
||||||
File "C:\Python27\lib\urllib.py", line 93, in urlretrieve
|
|
||||||
return _urlopener.retrieve(url, filename, reporthook, data)
|
|
||||||
File "C:\Python27\lib\urllib.py", line 283, in retrieve
|
|
||||||
"of %i bytes" % (read, size), result)
|
|
||||||
urllib.ContentTooShortError: retrieval incomplete: got only 15239952 out of 24807571 bytes
|
|
||||||
|
|
||||||
[ ] find out if urlretrieve may return unicode headers
|
|
||||||
[ ] test suite for unsafe filenames from url and from headers
|
|
||||||
|
|
||||||
[ ] security checks
|
|
||||||
[ ] filename_from_url
|
|
||||||
[ ] filename_from_headers
|
|
||||||
[ ] MITM redirect from https URL
|
|
||||||
[ ] https certificate check
|
|
||||||
[ ] size+hash check helpers
|
|
||||||
[ ] fail if size is known and mismatch
|
|
||||||
[ ] fail if hash mismatch
|
|
||||||
"""
|
|
Loading…
Reference in New Issue