Desktop App/C#.Net

압축된 폰트파일 한번에 풀고 설치하기

Jade Choe 2022. 6. 1. 17:52
SMALL

결과물:

FontInstaller.zip
0.49MB

 

깃 레포지토리: https://github.com/punch5545/FontInstaller

 

GitHub - punch5545/FontInstaller: 압축된 폰트파일을 한꺼번에 압축해제 후 설치하는 프로그램입니다.

압축된 폰트파일을 한꺼번에 압축해제 후 설치하는 프로그램입니다. Contribute to punch5545/FontInstaller development by creating an account on GitHub.

github.com

 

네이버 나눔폰트를 다운받고 압축을 푸니

이중, 삼중으로 압축되어 있다.

 

전부 풀어서 하나하나 설치하면 되긴 하지만...

디렉토리까지 꼼꼼하게 나눠주신 덕분에 매우 귀찮아서 하지말까.. 하는 생각이 들던 찰나

폰트통이라는 앱이 생각났다.

 

그거 다운받아서 거기서 나눔폰트 받아 쓰면 되지만 심심하기도 해서 직접 만들어보기로 했다.

 

 

레이아웃은 크게 중요하지 않으므로 대충 짜준다.

우측에는 현재 진행 상황을 볼 수 있게 로그를 찍고, 쓰다보니 안보이는 경우가 생겨서

SplitContainer안에 넣고 DataGridView와 TextBox 크기를 마우스로 조정할 수 있게 했다.

 

 

우선 폴더선택을 해야 한다.

바닐라로 제공되는 Dialog는 폴더가 노드형태로 보이고, 경로 찾아가기가 불편하게 되어있기 때문에 ApiCodePack을 사용했다.

using Microsoft.WindowsAPICodePack.Dialogs;


...


        private void button1_Click(object sender, EventArgs e)
        {
            CommonOpenFileDialog dialog = new CommonOpenFileDialog();

            dialog.IsFolderPicker = true;

            if(dialog.ShowDialog() == CommonFileDialogResult.Ok)
            {
                textBox1.Text = dialog.FileName;
            }
        }

 

검색시작 버튼을 누르면 검색하고, 압축풀고, ttf파일을 왼쪽의 DataGridView에 넣어줄 것이다.

 

하위폴더와 폴더 내 파일을 탐색하는 함수를 만들어준다.

        // 모든 하위 디렉토리 가져오기
        public List<DirectoryInfo> GetAllChildDirectories(string path)
        {
            DirectoryInfo info = new DirectoryInfo(path);
            List<DirectoryInfo> infos = new List<DirectoryInfo>();

            infos.Add(info);

            var dirs = info.GetDirectories();
            foreach (var dir in dirs)
            {
                if (dir.GetDirectories().Length > 0)
                {
                    var i = GetAllChildDirectories(dir.FullName);
                    infos.AddRange(i);
                }
                infos.Add(dir);
            }
            return infos;
        }

 

어려울 건 없다.

폴더 내 하위 디렉토리를 탐색하고, 각 하위 디렉토리에 디렉토리가 더 존재하면 자기자신을 한번 더 호출한다.

 

호출해서 리턴받은 결과를 최상위 디렉토리 탐색할때 선언한 infos에 AddRange 해주고, 자기 자신도 Add 해주면 모든 하위 디렉토리가 infos 안에 담겨 리턴된다.

 

        // 폴더 내 모든 파일 가져오기
        public List<FileInfo> GetChildItems(string path, params string[] extensions)
        {
            DirectoryInfo info = new DirectoryInfo(path);
            string ext = "";

            foreach(var extension in extensions)
            {
                ext += $"*.{extension}|";
            }
            ext = ext.TrimEnd('|');

            return info.GetFiles("*.*").Where(s=>extensions.Any(e=>s.Name.ToLower().EndsWith(e))).ToList();
        }

 

파일은 더 간단하다.

가변인자로 찾을 파일 확장자를 주면 모든 파일을 가져와 해당하는 확장자의 파일만 리턴해준다.

어차피 ttf, otf, zip파일을 반드시 필터링 해야해서 가변인자 없이 호출하는 함수는 안만들었다.

 

찾은 파일들을 복사해주기 위해 모든 파일을 복사하는 함수도 하나 만들어준다.

        public void CopyAllFiles(string targetDir, string destDir, params string[] extensions)
        {
            var dirs = GetAllChildDirectories(targetDir);
            var files = new List<FileInfo>();

            // 모든 파일 검색
            foreach (var dir in dirs)
            {
                var filelist = GetChildItems(dir.FullName, extensions);
                files.AddRange(filelist);
            }
            // 임시경로로 복사
            foreach (var file in files)
            {
                try
                {
                    if (file.Length > 2048)
                        file.CopyTo(destDir + "\\" + file.Name);
                }
                catch { }
            }
        }

중간에 file.Length는 파일 크기인데, 클로바 손글씨 폰트 압축파일 내에 1KB 미만의 파일들이 있어서 필터링해줬다.

파일명이 중복된 경우 복사하지 않고 넘어가도록 try catch로 감싸줬다.

 

폴더가 존재하지 않는다거나 읽기 전용인 경우 등의 오류를 대비해 로그까지 남기려고 했으나 귀찮아서 그냥 넘겼다.

 

 

        private async void button5_Click(object sender, EventArgs e)
        {
            if (textBox1.Text == "") return;

            var tmpDir = new DirectoryInfo(textBox1.Text).Parent.CreateSubdirectory("tmp");

            List<string> exts = new List<string>();
            exts.Add("zip");
            exts.Add("ttf");
            if(checkBox1.Checked) exts.Add("otf");

            DirectoryManager.Instance.CopyAllFiles(textBox1.Text, tmpDir.FullName, exts.ToArray());

 

이제 검색 시작 버튼을 누르면 선택한 디렉터리와 같은 레벨에 임시폴더를 만들고, 

zip파일과 ttf파일, 체크박스에 체크되어있으면 otf파일까지 싹 긁어서 임시폴더로 복사한다.

 

            var tmpFiles = DirectoryManager.Instance.GetChildItems(tmpDir.FullName, "zip");

            while (tmpFiles.Count != 0)
            {
                foreach (var tmpFile in tmpFiles)
                {
                    bool unzip = await Zipper.UnzipFile(tmpFile.FullName, tmpDir.FullName);
                    if (unzip)
                        tmpFile.Delete();

                }
                DirectoryManager.Instance.MoveAllFiles(tmpDir.FullName, tmpDir.FullName, exts.ToArray());

                tmpFiles = DirectoryManager.Instance.GetChildItems(tmpDir.FullName, "zip");
            }

임시폴더 내에 Zip파일이 있으면 압축을 풀어 임시폴더 Root 디렉토리로 전부 이동해준다.

하위 디렉토리 내에 Zip파일이 또 있으면 다음 싸이클에 압축 해제를 하고, 임시폴더 내의 파일들만 불러와주기 위함이다.

이 때 중복작업을 방지하기 위해 압축해제한 파일은 삭제해 준다.

어차피 원본 파일은 살아있으니 삭제해도 상관 없다.

 

그리곤 남아있는 zip파일이 있는지 확인해 반복 작업 해준다.

 

        public static async Task<bool> UnzipFile(string zipPath, string unzipPath)
        {
            await Task.Run(() => {
                try
                {
                    using (ZipFile zip = ZipFile.Read(zipPath))
                    {
                        FileInfo fi = new FileInfo(zipPath);

                        DirectoryInfo dir = new DirectoryInfo(unzipPath);

                        Main.Log(Path.GetFileName(zipPath) + " 압축해제중");

                        if (!dir.Exists)
                        {
                            dir.Create();
                        }

                        string saveFolderPath = $"{unzipPath}\\{Path.GetFileNameWithoutExtension(zipPath)}";

                        for (int i = 0; i < zip.Entries.Count; i++)
                        {
                            ZipEntry entry = zip[i];

                            byte[] byteIbm437 = Encoding.GetEncoding("IBM437").GetBytes(zip[i].FileName);

                            try
                            {
                                string fileName = Encoding.Default.GetString(byteIbm437);
                                zip[i].FileName = fileName;

                                entry.Extract(saveFolderPath.Replace("/", ""));
                            }
                            catch
                            {
                                string fileName = Encoding.UTF8.GetString(byteIbm437);
                                zip[i].FileName = fileName;

                                entry.Extract(saveFolderPath.Replace("/", ""));
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                    Main.Log($"{Path.GetFileName(zipPath)} 파일 압축 해제 실패 : {ex.Message}");
                }
            });

            return true;
        }

압축해제는 Ionic.Zip을 사용했는데,

System.IO.Compression에 있는 ZipArchive로 작성하다가 자꾸 권한이 없다며 예외를 던져대기에 짜증나서 바꿨다.

 

관리자 권한인데 권한이 없으면.. 뭐 니가 시스템 파일이세요?

 

            Log("압축해제 완료");

            DirectoryManager.Instance.RemoveAllEmptyChilds(tmpDir.FullName);

            exts.Remove("zip");

            var fonts = DirectoryManager.Instance.GetChildItems(tmpDir.FullName, exts.ToArray());

            foreach(var font in fonts)
            {
                PrivateFontCollection fontCol = new PrivateFontCollection();
                fontCol.AddFontFile(font.FullName);

                fontList.Add(new KeyValuePair<string, FileInfo>(fontCol.Families[0].Name, font));
            }

 

모든 파일의 압축해제가 끝나면 폴더 내의 모든 폰트파일을 불러다가 List에 등록해준다.

Key에는 폰트 이름, Value에는 폰트 FileInfo를 그대로 등록한다.

 

            dataGridView1.DataSource = fontList;
            dataGridView1.Columns[0].Width = 150;
            dataGridView1.Columns[1].Width = 300;

            foreach (DataGridViewRow row in dataGridView1.Rows)
            {
                var style = new DataGridViewCellStyle();
                PrivateFontCollection privateFonts = new PrivateFontCollection();
                privateFonts.AddFontFile(((FileInfo)row.Cells[1].Value).FullName);
                Font font = new Font(privateFonts.Families[0], 12f);
                style.Font = font;
                row.Cells[0].Style = style;
            }
       }

 

DataGridView의 데이터소스를 fontList로 설정해주고,

각 Row별 Cell Style을 해당 폰트로 바꿔준다.

 

Value에 폰트패밀리를 등록하지 않고 FileInfo를 넣어준 이유는

나중에 등록할때 폰트파일의 Full Path가 필요한데 폰트패밀리에는 Full Path 정보가 없다..ㅡㅡ..

 

 

아무튼 저 작업이 끝나고 나면

 

 

이렇게 파일명과 해당 폰트가 적용된 폰트명이 나온다.

 

 

모두설치를 누르면 이렇게 Log를 찍으며 설치해준다.

이미 설치된 폰트는 로그가 찍히지 않는다.

        [DllImport("gdi32.dll")]
        private static extern int AddFontResource(string fontFilePath);

        private static string _keyName = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts";

        public static void RegisterFont(string sourceFontFilePath)
        {
            string targetFontFileName = Path.GetFileName(sourceFontFilePath);
            string targetFontFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), targetFontFileName);

            if (!File.Exists(targetFontFilePath))
            {
                File.Copy(sourceFontFilePath, targetFontFilePath);

                PrivateFontCollection collection = new PrivateFontCollection();

                collection.AddFontFile(targetFontFilePath);

                string actualFontName = collection.Families[0].Name;

                AddFontResource(targetFontFilePath);

                Registry.SetValue(_keyName, actualFontName, targetFontFileName, RegistryValueKind.String);

                Main.Log($"{actualFontName} 설치완료");
            }
        }

 

워드에서 확인해보면 잘 설치된 것을 볼 수 있다.

 

무슨 이유에서인지 선택 삭제와 선택 설치가 안되는데, 조금 더 확인해 보도록 하고..

 

조금 더 심심하면 압축해제 진행상황 Progress Bar와 미리써보기 기능도 추가해 봐야겠다.

 

 

References

- c# WinForm에 외부 폰트 적용하기 : http://goodhelper.egloos.com/1813667

- Ionic.Zip.ZipFile 사용시 한글 파일명 깨짐 현상 막기 : http://hsouhy.egloos.com/v/1118418

- [C#/WINFORM] 폰트 설치하기 : https://icodebroker.tistory.com/4408

 

BIG