사진 섬네일 만들기

간단하게 구현해 보았습니다.

주요 부분을 설명하자면

// .NET Framework 에서 제공하는 Built-in JPEG 인코더를 구해서
var jpegEncoder = ImageCodecInfo.GetImageEncoders().Where(info => info.FormatID == ImageFormat.Jpeg.Guid).FirstOrDefault();
// 인코딩 파라미터로 화질을 결정하고
var encoderParams = new EncoderParameters(1);
var encoderParam = new EncoderParameter(Encoder.Quality, 50L);
encoderParams.Param[0] = encoderParam;
// 원본을 읽어서
var srcImage = new Bitmap(@"C:\Photo\SourceFile.JPG");
// 리사이징 한 후에
var newImage = new Bitmap(srcImage, newSize);
// 원본의 EXIF 정보를 그대로 옮기고
foreach (var propItem in srcImage.PropertyItems)
  newImage.SetPropertyItem(propItem);
// 저장 합니다
newImage.Save(@"C:\Photo\Thumbnail.JPG", jpegEncoder, encoderParams);
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace BSPFP {
  class BSException : Exception {
    public BSException() : base() { }
    public BSException(string msg) : base(msg) { }
    public BSException(string fmt, params object[] args) : base(string.Format(fmt, args)) { }
  }

  class Program {
    static readonly int longSideLength = 640; // 섬네일의 긴쪽 길이
    static readonly long quality = 50L; // 섬네일 화질 (0 - 100)
    static readonly HashSet<string> srcFolders = new HashSet<string> {
      Path.GetFullPath(Path.Combine(@"C:\Photo\사진 원본 1", ".")),
      Path.GetFullPath(Path.Combine(@"C:\Photo\사진 원본 2", ".")),
    };
    static readonly string destFolder = Path.GetFullPath(Path.Combine(@"C:\Photo\사진 섬네일", "."));
    static readonly HashSet<string> targetExt = new HashSet<string> { ".jpeg", ".jpg" };

    static readonly int concurrentCount = Environment.ProcessorCount;

    static void CheckReadOnlyVariables() {
      foreach (var srcFolder in srcFolders) {
        if (string.Compare(srcFolder, destFolder, true) == 0)
          throw new BSException("원본, 대상 폴더가 같음: {0}: {1}", srcFolder, destFolder);
        if (destFolder.ToLower().StartsWith(srcFolder.ToLower()))
          throw new BSException("대상이 원본 하위 폴더: {0}: {1}", srcFolder, destFolder);
      }
    }

    static ConcurrentQueue<Tuple<string, string>> CollectSourceFiles() {
      var ret = new ConcurrentQueue<Tuple<string, string>>();
      foreach (var srcFolder in srcFolders) {
        var folderQueue = new Queue<string>();
        folderQueue.Enqueue(srcFolder);
        while (folderQueue.Count > 0) {
          var f = folderQueue.Dequeue();
          foreach (var e in Directory.EnumerateFileSystemEntries(f)) {
            var fi = new FileInfo(e);
            if ((fi.Attributes & FileAttributes.Directory) != 0) {
              folderQueue.Enqueue(e);
              continue;
            }
            if (!targetExt.Contains(fi.Extension.ToLower()))
              continue;
            ret.Enqueue(new Tuple<string, string>(srcFolder, fi.FullName));
          }
        }
      }
      return ret;
    }

    static int Main() {
      try {
        CheckReadOnlyVariables();
        var srcFiles = CollectSourceFiles();

        var tasks = new Task[concurrentCount];
        for (var i = 0; i < concurrentCount; i++) { tasks[i] = Task.Factory.StartNew(() => {
            var jpegEncoder = ImageCodecInfo.GetImageEncoders().Where(info => info.FormatID == ImageFormat.Jpeg.Guid).FirstOrDefault();
            using (var encoderParams = new EncoderParameters(1))
            using (var encoderParam = new EncoderParameter(Encoder.Quality, quality)) {
              encoderParams.Param[0] = encoderParam;
              while (srcFiles.TryDequeue(out Tuple<string, string> src)) {
                using (var srcImage = new Bitmap(src.Item2)) {
                  Size newSize;
                  if (srcImage.Size.Width >= srcImage.Size.Height)
                    newSize = new Size(longSideLength, (int)((double)srcImage.Size.Height * longSideLength / srcImage.Size.Width));
                  else
                    newSize = new Size((int)((double)srcImage.Size.Width * longSideLength / srcImage.Size.Height), longSideLength);
                  using (var newImage = new Bitmap(srcImage, newSize)) {
                    foreach (var propItem in srcImage.PropertyItems)
                      newImage.SetPropertyItem(propItem);
                    var subPath = src.Item2.Substring(src.Item1.Length + 1);
                    var folder = Path.GetDirectoryName(Path.GetFullPath(Path.Combine(destFolder, subPath)));
                    var filename = Path.GetFileNameWithoutExtension(src.Item2);
                    var destFile = Path.Combine(folder, filename + ".jpg");
                    if (!Directory.Exists(folder))
                      Directory.CreateDirectory(folder);
                    Console.WriteLine(subPath);
                    newImage.Save(destFile, jpegEncoder, encoderParams);
                  }
                }
              }
            }
          });
        }

        Task.WaitAll(tasks);
        for (var i = 0; i < concurrentCount; i++)
          tasks[i].Dispose();
      } catch (Exception ex) {
        Console.WriteLine(ex.Message);
        Console.WriteLine(ex.StackTrace);
        return 1;
      }
      return 0;
    }
  }
}

어떤 Type이 익명 타입(Anonymous type)인지 검사하는 방법

System.Type 인스턴스로부터 해당 타입이 익명 타입인지 검사하는 방법이… 아직은 없는 듯 합니다.


그래서 사람들이 좀 쓴다는 방법이…



  • CompilerGeneratedAttribute 특성이 있고

  • 제네릭 타입 이고

  • 이름에 AnonymousType 이 들어가 있고

  • 이름이 <> 또는 VB$로 시작하면서

  • public으로 접근이 안됨

위 조건을 만족하면 익명 타입이라고 하네요.

여기서 잠깐!!! 익명 타입이라는 건…

var some = new { data1 = 0, data2 = “string” };

요런 식으로 쓰는 그 것!!!


그럼 위 검사를 [Jef Claes님의 방법]으로 구현하면 아래와 같습니다.
(사실 내용을 보면… ASP.NET MVC source를 뒤지다가 발견했다고 적혀있습니다.)

public static bool IsAnonymousType(Type type)
{
    if (type == null)
    {
        throw new Error("type sould be not null");
    }
    
    return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false)
        && type.IsGenericType && type.Name.Contains("AnonymousType")
        && (type.Name.StartsWith("<>", StringComparison.OrdinalIgnoreCase) ||
            type.Name.StartsWith("VB$", StringComparison.OrdinalIgnoreCase))
        && (type.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic;
}

현재 소스 줄 번호를 출력하고 싶습니다.

아주 간단하게 소스로 설명 하자면…


1. 함수 인자의 특성으로 만들기

public void TraceMessage(string message,
        [CallerMemberName] string callingMethod = string.Empty,
        [CallerFilePath] string callingFilePath = string.Empty,
        [CallerLineNumber] int callingFileLineNumber = 0)
{
    // Write out message
}


2. 콜 스택에서 가져오기1

private static void ReportError(string message)
{
     StackFrame callStack = new StackFrame(1, true);
     MessageBox.Show("Error: " + message + ", File: " + callStack.GetFileName() 
          + ", Line: " + callStack.GetFileLineNumber());
}


3. 콜 스택에서 가져오기2

int lineNumber = (new System.Diagnostics.StackFrame(0, true)).GetFileLineNumber();


4. 예외를 가지고 만들기

try
{
    //Do something
}
catch (Exception ex)
{
    System.Diagnostics.StackTrace trace = new System.Diagnostics.StackTrace(ex, true);
    Console.WriteLine("Line: " + trace.GetFrame(0).GetFileLineNumber());
}


1번이 제일 깔끔해 보이네요. 단 해당 특성은 .NET Framework 4.5이상만 가능하다는… 이 외의 환경에서는 2번을 추천합니다.

.NET 어셈블리를 하나로 묶는 방법

간단한 클래스 하나를 .NET DLL로 만들고 이를 사용하는 초간단 응용프로그램을 하나 만들었습니다.
그런데 매번 실행 파일과 DLL을 가지고 다니기가 너무 귀찮군요.
그래서 찾아보니 아래와 같은 자료가 있더군요.


[ILMerge 설명 페이지]
[ILMerge 다운로드 링크]


콘솔 응용 프로그램이군요.
아… 뭐 이리 복잡해!!!
그런데 위 페이지에 소개된 다른 방법도 있군요.


[MSDN Blogs 포스트]


그럼 이 방법으로 해보겠습니다.


우선 솔루션 탐색기에서 프로젝트를 우클릭하여, 추가 > 기존항목 추가를 통해 DLL을 추가합니다.
그리고 추가된 DLL의 속성을 열어보면 빌드 작업이란 항목이 있는데 거기에서 포함 리소스(Embedded Resource)로 변경합니다.
그리고 어플리케이션에서 아래와 같은 방법으로 어셈블리를 참조할 수 있도록 해줍니다.

            AppDomain.CurrentDomain.AssemblyResolve += (sender, bargs) =>
            {
                string dllName = new AssemblyName(bargs.Name).Name + ".dll";
                var assem = Assembly.GetExecutingAssembly();
                string resourceName = null;
                foreach (string str in assem.GetManifestResourceNames())
                {
                    if (str.IndexOf(dllName) != -1)
                    {
                        resourceName = str;
                        break;
                    }
                }
                if (resourceName == null) return null;
                using (var stream = assem.GetManifestResourceStream(resourceName))
                {
                    Byte[] assemblyData = new Byte[stream.Length];
                    stream.Read(assemblyData, 0, assemblyData.Length);
                    return Assembly.Load(assemblyData);
                }
            };

테스트 환경은 Visual Studio 2012이고 대상 프레임워크 버전은 2.0 입니다. (유니티 때문에 2.0으로….)

true라고 같은 true는 아닙니다.

JaredPar님의 블로그를 갔다가 본 글입니다.
[원문 보기]

아래 코드를 보면 한방에 “아하”하고 알 수 있는 내용입니다.

class Program
{
	[StructLayout(LayoutKind.Explicit)]
	struct Union
	{
		[FieldOffset(0)]
		internal byte ByteField;

		[FieldOffset(0)]
		internal bool BoolField;
	}

	static void Main(string[] args)
	{
		Union u1 = new Union();
		Union u2 = new Union();
		u1.ByteField = 1;
		u2.ByteField = 2;
		Console.WriteLine(u1.BoolField); // True
		Console.WriteLine(u2.BoolField); // True
		Console.WriteLine(u1.BoolField == u2.BoolField); // False
	}
}

간단한 내용 같지만 정말 주의해야 하는 내용입니다.
여러분이 사용하시는 어떤 라이브러리에서 저런 코드가 있다면

if (someBoolVar == someBoolVar2)

이렇게 비교할 수 없다는 것이니까요.

C#으로 키보드 후킹하기

세이버 개조된 키보드를 위한 프로그램을 오늘 수정하였습니다.
간만에 체리 미니 키보드의 봉인을 해제하였는데 Ctrl+F11을 입력할 수 없는 키보드라서 이전에는 레지스트리를 수정해서 매핑을 바꿨습니다.
그런데 레지스트리를 매번 수정하는 것도 좀 그렇고 내 컴퓨터가 아닌 곳에서 사용하기도 좀 그렇고…
그래서 이번에는 C#으로 작성한 KeyLED 프로그램에 후킹을 넣어 보았습니다.

우선 API에서 사용할 콜백 함수에 대한 대리자를 선언합니다.

// LRESULT CALLBACK HOOKPROC(int code, WPARAM wparam, LPARAM lparam);
delegate IntPtr HOOKPROC(int code, UIntPtr wparam, UIntPtr lparam);

DLL에서 Win32 API 몇개를 가져옵니다.

// VOID WINAPI keybd_event(BYTE bVk, BYTE bScan, DWORD dwFlags, ULONG_PTR dwExtraInfo);
[DllImport("user32.dll")]
static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);

// HMODULE WINAPI GetModuleHandle(LPCTSTR moduleName);
// 소스에서 moduleName에 0을 넣어야 하기 때문에 UIntPtr로 가져옵니다. 보통은 LPCTSTR은 string으로 가져오지요...
[DllImport("kernel32.dll")]
static extern UIntPtr GetModuleHandle(UIntPtr moduleName);

// HHOOK WINAPI SetWindowsHookEx(int idHook, HOOKPROC lpfn, HINSTANCE hmod, DWORD threadID);
// 콜백 함수는 위에서 선언한 대리자로...
[DllImport("user32.dll")]
static extern UIntPtr SetWindowsHookEx(int idHook, HOOKPROC lpfn, UIntPtr hmod, uint threadID);

// BOOL WINAPI UnhookWindowsHookEx(HHOOK hhk);
[DllImport("user32.dll")]
static extern bool UnhookWindowsHookEx(UIntPtr hhk);

// LRESULT WINAPI CallNextHookEx(HHOOK hhk, int code, WPARAM wparam, LPARAM lparam);
[DllImport("user32.dll")]
static extern IntPtr CallNextHookEx(UIntPtr hhk, int code, UIntPtr wparam, UIntPtr lparam);

// UINT WINAPI MapVirtualKey(UINT code, UINT type);
[DllImport("user32.dll")]
static extern uint MapVirtualKey(uint code, uint type);

그리고 사용할 상수도 몇개 정의합니다.

const byte VK_HANJA = 0x19;
const byte VK_LCONTROL = 0xa2;
const byte VK_RWIN = 0x5c;
const uint KEYEVENTF_KEYUP = 0x0002;
const int WH_KEYBOARD_LL = 13;
const uint MAPVK_VK_TO_VSC = 0;
const uint KF_UP = 0x8000;
const uint LLKHF_UP = KF_UP >> 8;
const int HC_ACTION = 0;

구조체도 하나 정의합니다.

struct KBDLLHOOKSTRUCT
{
	public uint vkCode;
	public uint scanCode;
	public uint flags;
	public uint time;
	public UIntPtr dwExtraInfo;
}

후킹은 이렇게 하면 됩니다.

hhk = SetWindowsHookEx(WH_KEYBOARD_LL, new HOOKPROC(cherryCtrlHookProc), GetModuleHandle((UIntPtr)0), 0);
if (hhk == (UIntPtr)0)
	MessageBox.Show("Failed: Set Hook");

후킹 취소는 이렇게 하면 됩니다.

UnhookWindowsHookEx(hhk);

후킹 프로시저의 모습입니다.
내용을 보면 한자키->왼쪽 컨트롤키, 오른쪽 윈도우키->한자키 로 변경합니다.
중간에 dwExtraInfo로 걸러내는 이유는 오른쪽 윈도우키->한자키->왼쪽 컨트롤키로 변환되지 않게 하기 위해서 입니다.

private IntPtr cherryCtrlHookProc(int code, UIntPtr wparam, UIntPtr lparam)
{
	if (code == HC_ACTION)
	{
		byte vk;
		byte scan;
		uint flag;
		UIntPtr extra;

		unsafe
		{
			KBDLLHOOKSTRUCT* khs = (KBDLLHOOKSTRUCT*)lparam;

			if (khs->dwExtraInfo == (UIntPtr)1)
				return CallNextHookEx((UIntPtr)0, code, wparam, lparam);

			if (khs->vkCode == VK_HANJA)
				vk = VK_LCONTROL;
			else if (khs->vkCode == VK_RWIN)
				vk = VK_HANJA;
			else
				return CallNextHookEx((UIntPtr)0, code, wparam, lparam);
			extra = (UIntPtr)1;
			flag = ((khs->flags & LLKHF_UP) != 0) ? KEYEVENTF_KEYUP : 0;
		}
		scan = (byte)MapVirtualKey(vk, MAPVK_VK_TO_VSC);
		keybd_event(vk, scan, flag, extra);
		return (IntPtr)1;
	}
	return CallNextHookEx((UIntPtr)0, code, wparam, lparam);
}

아… 반드시 /unsafe 옵션이 활성화 되어야 합니다.
이 옵션이 없으면 unsafe 블럭을 사용할 수 없습니다.