From f2c8610ff61ae8231a9b757089abe100d95f5e3f Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Wed, 6 May 2026 13:03:16 +0900 Subject: [PATCH] =?UTF-8?q?Phase=202:=20MSIX=20=ED=8C=A8=ED=82=A4=EC=A7=80?= =?UTF-8?q?=20+=20IExplorerCommand=20=EC=85=B8=20=EC=9D=B5=EC=8A=A4?= =?UTF-8?q?=ED=85=90=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C++ 셸 익스텐션 (src/EverythingToJpeg.Shell/) - WRL RuntimeClass 패턴으로 IExplorerCommand 두 핸들러 구현 - Quick(빠른 변환) / Dialog(설정 창) verb를 다른 CLSID로 분리 - Invoke()에서 EverythingToJpeg.exe로 verb + 파일 경로 전달 - VS 2026 빌드 검증, /utf-8 한글 라벨 지원 MSIX 패키징 (packaging/) - Package.appxmanifest: com:SurrogateServer + desktop4:FileExplorerContextMenus - 26개 확장자 × 2 verb 자동 노출 - BuildMsix.ps1: dotnet publish + msbuild + makeappx 일관 파이프라인 - CreateDevCert.ps1 / Install-EverythingToJpeg.ps1: 자체 서명 인증서 워크플로 - GenerateAssets.ps1: placeholder 로고 자동 생성 CI/CD - .github/workflows/release.yml: 태그 푸시 시 미서명 MSIX 자동 빌드 + 첨부 검증 - 50MB MSIX 산출 확인 (packaging/dist/EverythingToJpeg-x64.msix) Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release.yml | 55 +++++ .gitignore | 27 +++ README.md | 37 ++-- packaging/Assets/Square150x150Logo.png | Bin 0 -> 1565 bytes packaging/Assets/Square44x44Logo.png | Bin 0 -> 670 bytes packaging/Assets/StoreLogo.png | Bin 0 -> 729 bytes packaging/Assets/Wide310x150Logo.png | Bin 0 -> 4901 bytes packaging/BuildMsix.ps1 | 138 +++++++++++++ packaging/CreateDevCert.ps1 | 44 ++++ packaging/GenerateAssets.ps1 | 56 ++++++ packaging/Install-EverythingToJpeg.ps1 | 41 ++++ packaging/Package.appxmanifest | 182 +++++++++++++++++ packaging/README.md | 102 ++++++++-- .../EverythingToJpeg.Shell.vcxproj | 110 ++++++++++ src/EverythingToJpeg.Shell/Source.def | 5 + src/EverythingToJpeg.Shell/dllmain.cpp | 188 ++++++++++++++++++ src/EverythingToJpeg.Shell/framework.h | 4 + src/EverythingToJpeg.Shell/pch.cpp | 1 + src/EverythingToJpeg.Shell/pch.h | 24 +++ 19 files changed, 973 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 packaging/Assets/Square150x150Logo.png create mode 100644 packaging/Assets/Square44x44Logo.png create mode 100644 packaging/Assets/StoreLogo.png create mode 100644 packaging/Assets/Wide310x150Logo.png create mode 100644 packaging/BuildMsix.ps1 create mode 100644 packaging/CreateDevCert.ps1 create mode 100644 packaging/GenerateAssets.ps1 create mode 100644 packaging/Install-EverythingToJpeg.ps1 create mode 100644 packaging/Package.appxmanifest create mode 100644 src/EverythingToJpeg.Shell/EverythingToJpeg.Shell.vcxproj create mode 100644 src/EverythingToJpeg.Shell/Source.def create mode 100644 src/EverythingToJpeg.Shell/dllmain.cpp create mode 100644 src/EverythingToJpeg.Shell/framework.h create mode 100644 src/EverythingToJpeg.Shell/pch.cpp create mode 100644 src/EverythingToJpeg.Shell/pch.h diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..82f0554 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,55 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Build MSIX (unsigned) + shell: pwsh + run: | + ./packaging/GenerateAssets.ps1 + ./packaging/BuildMsix.ps1 -Configuration Release -Platform x64 + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: EverythingToJpeg-x64-msix + path: packaging/dist/EverythingToJpeg-x64.msix + + - name: Create GitHub Release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + files: packaging/dist/EverythingToJpeg-x64.msix + generate_release_notes: true + body: | + ## 설치 방법 + + 1. 본 릴리즈에서 `EverythingToJpeg-x64.msix` 와 함께 배포된 PFX 인증서를 받습니다. + 2. 관리자 PowerShell: + ```powershell + .\Install-EverythingToJpeg.ps1 -PfxPath .\EverythingToJpeg-DevCert.pfx -MsixPath .\EverythingToJpeg-x64.msix + ``` + 3. PNG/HEIC/PDF 등 파일을 우클릭 → "JPEG로 빠른 변환" 또는 "JPEG로 변환…" + + > 이 패키지는 자체 서명입니다. 인증서를 `LocalMachine\TrustedPeople`에 신뢰 등록해야 설치됩니다. diff --git a/.gitignore b/.gitignore index 5451a16..a02eac6 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,30 @@ Thumbs.db # rider .idea/ + +# Phase 2 packaging artifacts +*.pfx +*.msix +*.msixbundle +*.appx +*.appxbundle +packaging/Layout/ +packaging/dist/ + +# C++ project intermediate +src/EverythingToJpeg.Shell/x64/ +src/EverythingToJpeg.Shell/Win32/ +src/EverythingToJpeg.Shell/Debug/ +src/EverythingToJpeg.Shell/Release/ +src/EverythingToJpeg.Shell/.vs/ +src/EverythingToJpeg.Shell/Everythi*/ +*.tlog +*.obj +*.pch +*.iobj +*.ipdb +*.recipe +*.lastbuildstate + +# .NET artifacts dir from BuildMsix.ps1 +artifacts/ diff --git a/README.md b/README.md index 89e58fa..09f9e37 100644 --- a/README.md +++ b/README.md @@ -59,31 +59,26 @@ dotnet publish src\EverythingToJpeg.App\EverythingToJpeg.App.csproj ` - UI: **WPF-UI 4.3** (Win11 Fluent 2 — Mica 백드롭, Segoe UI Variable 타입 램프) - 변환 엔진: Magick.NET, PDFtoImage, PhotoSauce.MagicScaler + Libheif -## 로드맵 (Phase 2) +## 로드맵 -1. **IExplorerCommand 셸 익스텐션 + MSIX Sparse Package** — Win11 메인 컨텍스트 메뉴 직접 노출 -2. **HTML 변환** — WebView2 헤드리스, viewport 옵션 -3. **HWP/HWPX 변환** — LibreOffice + H2Orestart 자동 설치 가이드 -4. **GitHub Releases CI/CD** — 태그 푸시 시 자동 빌드 + MSIX 패키징 -5. **자체 서명 인증서 자동 생성·배포** — 내부 5대 PC 신뢰 체인 자동화 (현재는 unsigned MSIX → 개발자 모드 필요) +| 단계 | 상태 | 내용 | +|---|---|---| +| Phase 1 | ✅ | 레지스트리 컨텍스트 메뉴 (Win11 "추가 옵션 표시"), 핵심 변환(이미지·HEIC·RAW·PDF·DOCX), Fluent UI | +| Phase 2 | ✅ 빌드 가능 | C++ IExplorerCommand DLL, MSIX 패키징, 자체 서명 인증서, GitHub Releases 자동화 — `packaging/README.md` 참조 | +| Phase 3 | 🕐 | HTML(WebView2), HWP/HWPX(LibreOffice + H2Orestart) 실구현 | -## 미서명 빌드를 신뢰할 PC에 설치하기 (Phase 2 미리보기) +## 두 가지 사용 방식 -본인 PC 5대에만 설치할 계획이므로 정식 코드사이닝 인증서 없이도 사용 가능합니다. +### A) Portable EXE — 가장 가벼움 (Phase 1) +- `dotnet publish` 산출물 그대로 사용 +- 우클릭 → **추가 옵션 표시** → "JPEG로 빠른 변환" / "JPEG로 변환…" +- 인증서·서명 불필요 -### 옵션 A — Portable EXE (지금 바로 가능) -1. `publish` 폴더 통째로 PC에 복사 -2. `EverythingToJpeg.exe` 실행 → 한 번만 "컨텍스트 메뉴 등록" -3. 끝. SmartScreen 경고가 뜨면 "추가 정보" → "실행" - -### 옵션 B — MSIX Sparse Package (Phase 2) -1. `EverythingToJpeg.Package` 프로젝트로 unsigned MSIX 빌드 -2. 각 PC에서 **개발자 모드 켜기** (설정 → 개인 정보 및 보안 → 개발자용) -3. PowerShell: - ```powershell - Add-AppxPackage -AllowUnsigned -Path EverythingToJpeg.msix - ``` -4. 또는 Group Policy로 사이드로딩 허용 후 자체 서명 인증서를 Local Machine\Trusted People에 임포트 +### B) MSIX 패키지 — Win11 메인 메뉴 노출 (Phase 2) +- `packaging/BuildMsix.ps1` 로 MSIX 빌드 +- 자체 서명 인증서를 `LocalMachine\TrustedPeople`에 임포트 후 사이드로드 +- 우클릭 → 바로 메인 메뉴에 항목 노출 +- 자세한 절차는 [packaging/README.md](packaging/README.md) ## 프로젝트 구조 diff --git a/packaging/Assets/Square150x150Logo.png b/packaging/Assets/Square150x150Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bd402c6a4b96c10707ccdc3440742c256ea710a7 GIT binary patch literal 1565 zcmdT^>sQhT82$lasj15n&2Xz0r%cnSsk2OHWy-YdkkF4!5e#G^QqJQAY17QhDknuk zlS0L;lv}_$ni7dh?KCgwR2(L@GqZ~+-a^t+N9XLH=)?0q&-3B^@|^d5E*w7=8AE<7-X`?cLKWXU;QwR1%Um#wPgYT`QQArj4_hmzjmpwX zx=ALh2hU?%CtM@sK07YO3RJFOn>JRAr9M9E-X~ZHqVvM(b zhgvs^swz2baZ7i(2$|CCcnWJuTy@M@lU;9&o|__mPh88u)BHF;epgmh$uBGV>0#h2 zrF$9I#Nbo%RKm{QN0foJNsRhRft<4jTfG>_<_o7}GTgI8(S+;59Ku#K#x-$#DQF+8{xPQkFpP3Sm z6Nh>9309s?UL`S)al`B0=k|K*P^nB9XW`0(ud(50CeBQ%Hj(eviNcPDpv*<0D9q^e zm5QYONCgJaHb>i4Cg(tQIm*2_jiTM!zf zURIejd*Y;=d#zY!OH*`mxd`w43=@bhq$S<-QAJc?D9b60mfMne%8KY|W=GQ+$VB1)Yp3qn+St=`D3nK~HuALDZZ64##7$W@{PC!E^Pr#Ad)A)IbsP z0=PF3@<~a_uHr%8B&R2Zw@vNN1=vv9a#Lu)b{l!E3+_%uvm}2!z~>qnW)JFZ?Zo82 zmij_c66hKzF9%98qXp(eB;CD=DmX%p@ZjUQqOTRWARJ0Z%ngca>H&oO1x&o-txTu3@p`#Uw$V zNXZNpLnnaMrTZHBI*`}B3!F!?{G235oFw*b+2`j4@OIjRj;p;IemVk3tBLbxYCi{m zk0Z-(aQ8e=K|tZ-mc7kKzde6rQ@z4<9&;IGbE&EOlReu1pG@CTV)ivFW8Gd>$G@{y OI)IKmc35_hSok04U9A-W literal 0 HcmV?d00001 diff --git a/packaging/Assets/Square44x44Logo.png b/packaging/Assets/Square44x44Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..997ce9960f655684b6ab4f7609a54e6036830f08 GIT binary patch literal 670 zcmV;P0%84$P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0y9ZOK~z{r?bY2+ z(qR+_aQ_p+pGbs=L_Z0=$x%5g=d2v1!?I#l4tsG_jMPK@5#H3gf$Lt#jLJC+bB>Zs zlbJ6mNXbws8CqB01~MaNw}IPvwsZ5G=i;-ov+q+-d3WQ0-jN`EcbxQ{KS*bfkei`LsmuXVo&Q9tmhn_6 z&y11M-eq&V#(g>i)B#$l2-43X@_8sPT|AS=D_c zJ=9AQgPSCV$0VtGNClOS1XVZ^sOXME-?fJQx4%PYUxm)LjPhGil;4Ozv@SvH_==%N-s=8b#4NxmT{Dv9Yaa8A4=0G_B6eN zqR|J%sS)fp4ny8BgyNH46d!*LS^Xf2>IP6$+YjN`D+otkps>b+!sH22BCl|4n4Suk;sr?ZAXAV0?lMhxvInQCl;qv^-zD%dY zB%E9Mybh=BzkeJ%?x$lggBi?V1~Zt!3}!Hc8Jr97Z~8MrE~u7tdjJ3c07*qoM6N<$ Eg54S_GXMYp literal 0 HcmV?d00001 diff --git a/packaging/Assets/StoreLogo.png b/packaging/Assets/StoreLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..a052a96be22531a746cd52cbbfa85248e80d2d24 GIT binary patch literal 729 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 literal 0 HcmV?d00001 diff --git a/packaging/BuildMsix.ps1 b/packaging/BuildMsix.ps1 new file mode 100644 index 0000000..86087dd --- /dev/null +++ b/packaging/BuildMsix.ps1 @@ -0,0 +1,138 @@ +#Requires -Version 5.1 +# MSIX 빌드 파이프라인. +# 1) .NET App publish (framework-dependent) +# 2) C++ Shell DLL 빌드 +# 3) 패키지 Layout 디렉토리 구성 +# 4) makeappx pack +# 5) (선택) signtool sign + +[CmdletBinding()] +param( + [string]$Configuration = 'Release', + [string]$Platform = 'x64', + [switch]$Sign, + [string]$PfxPath, + [securestring]$PfxPassword, + [string]$CertThumbprint, + [switch]$SelfContained +) + +$ErrorActionPreference = 'Stop' +$repoRoot = Split-Path -Parent $PSScriptRoot +$packagingDir = $PSScriptRoot +$layoutDir = Join-Path $packagingDir 'Layout' +$distDir = Join-Path $packagingDir 'dist' +$manifestPath = Join-Path $packagingDir 'Package.appxmanifest' +$assetsSrc = Join-Path $packagingDir 'Assets' + +$appProj = Join-Path $repoRoot 'src\EverythingToJpeg.App\EverythingToJpeg.App.csproj' +$shellProj = Join-Path $repoRoot 'src\EverythingToJpeg.Shell\EverythingToJpeg.Shell.vcxproj' + +function Find-WindowsSdkTool { + param([string]$ToolName) + $sdkRoots = @( + "${env:ProgramFiles(x86)}\Windows Kits\10\bin", + "$env:ProgramFiles\Windows Kits\10\bin" + ) | Where-Object { Test-Path $_ } + foreach ($root in $sdkRoots) { + $versions = Get-ChildItem -Path $root -Directory -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match '^\d+\.\d+\.\d+\.\d+$' } | + Sort-Object Name -Descending + foreach ($v in $versions) { + $candidate = Join-Path $v.FullName "x64\$ToolName" + if (Test-Path $candidate) { return [string]$candidate } + } + } + return $null +} + +$makeappx = Find-WindowsSdkTool 'makeappx.exe' +if (-not $makeappx) { throw 'makeappx.exe를 찾지 못했습니다. Windows 10 SDK가 필요합니다.' } +Write-Host "makeappx: $makeappx" + +if ($Sign) { + $signtool = Find-WindowsSdkTool 'signtool.exe' + if (-not $signtool) { throw 'signtool.exe를 찾지 못했습니다.' } + Write-Host "signtool: $signtool" +} + +$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" +if (-not (Test-Path $vswhere)) { throw 'vswhere.exe를 찾지 못했습니다.' } +$msbuild = (& $vswhere -latest -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1) +if (-not $msbuild) { throw 'MSBuild를 찾지 못했습니다.' } +Write-Host "msbuild: $msbuild" + +# ---- 1) .NET App publish ---- +Write-Host '' +Write-Host '[1/5] .NET App publish' +$publishOut = Join-Path $repoRoot ('artifacts\publish\app-' + $Platform.ToLower()) +if (Test-Path $publishOut) { Remove-Item $publishOut -Recurse -Force } +$rid = if ($Platform -eq 'ARM64') { 'win-arm64' } else { 'win-x64' } +$selfFlag = if ($SelfContained) { 'true' } else { 'false' } +& dotnet publish $appProj -c $Configuration -r $rid --self-contained $selfFlag -o $publishOut | Out-Host +if ($LASTEXITCODE -ne 0) { throw 'dotnet publish 실패' } + +# ---- 2) C++ Shell DLL ---- +Write-Host '' +Write-Host '[2/5] C++ Shell DLL 빌드' +& $msbuild $shellProj /t:Restore /p:RestorePackagesConfig=true /p:Configuration=$Configuration /p:Platform=$Platform /v:minimal | Out-Host +if ($LASTEXITCODE -ne 0) { throw 'Shell restore 실패' } +& $msbuild $shellProj /p:Configuration=$Configuration /p:Platform=$Platform /m /v:minimal | Out-Host +if ($LASTEXITCODE -ne 0) { throw 'Shell build 실패' } +$shellDll = Join-Path $repoRoot ("src\EverythingToJpeg.Shell\$Platform\$Configuration\EverythingToJpeg.Shell.dll") +if (-not (Test-Path $shellDll)) { throw "Shell DLL 산출물 없음: $shellDll" } + +# ---- 3) Layout 디렉토리 ---- +Write-Host '' +Write-Host '[3/5] Layout 디렉토리 구성' +if (Test-Path $layoutDir) { Remove-Item $layoutDir -Recurse -Force } +New-Item -ItemType Directory -Path $layoutDir | Out-Null + +Copy-Item -Path (Join-Path $publishOut '*') -Destination $layoutDir -Recurse -Force +Copy-Item -Path $shellDll -Destination $layoutDir -Force + +$layoutAssets = Join-Path $layoutDir 'Assets' +New-Item -ItemType Directory -Path $layoutAssets -Force | Out-Null +Copy-Item -Path (Join-Path $assetsSrc '*') -Destination $layoutAssets -Force + +Copy-Item -Path $manifestPath -Destination (Join-Path $layoutDir 'AppxManifest.xml') -Force + +# ---- 4) makeappx pack ---- +Write-Host '' +Write-Host '[4/5] makeappx pack' +if (-not (Test-Path $distDir)) { New-Item -ItemType Directory -Path $distDir | Out-Null } +$msixPath = Join-Path $distDir ("EverythingToJpeg-$($Platform.ToLower()).msix") +if (Test-Path $msixPath) { Remove-Item $msixPath -Force } +& $makeappx pack /d $layoutDir /p $msixPath /o | Out-Host +if ($LASTEXITCODE -ne 0) { throw 'makeappx pack 실패' } +Write-Host "✅ MSIX 산출: $msixPath" + +# ---- 5) (선택) sign ---- +if ($Sign) { + Write-Host '' + Write-Host '[5/5] signtool sign' + if ($CertThumbprint) { + & $signtool sign /fd SHA256 /sha1 $CertThumbprint /tr 'http://timestamp.digicert.com' /td SHA256 $msixPath | Out-Host + } elseif ($PfxPath) { + if (-not $PfxPassword) { + $PfxPassword = Read-Host -AsSecureString -Prompt 'PFX 비밀번호' + } + $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($PfxPassword) + try { + $plain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) + & $signtool sign /fd SHA256 /a /f $PfxPath /p $plain /tr 'http://timestamp.digicert.com' /td SHA256 $msixPath | Out-Host + } finally { + [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) + } + } else { + throw '서명을 하려면 -CertThumbprint 또는 -PfxPath 가 필요합니다.' + } + if ($LASTEXITCODE -ne 0) { throw 'signtool sign 실패' } + Write-Host '✅ 서명 완료' +} else { + Write-Host '' + Write-Host '[5/5] 서명 건너뜀 (-Sign 미지정). 사이드로드 시 인증서 필요.' +} + +Write-Host '' +Write-Host "최종 산출물: $msixPath" diff --git a/packaging/CreateDevCert.ps1 b/packaging/CreateDevCert.ps1 new file mode 100644 index 0000000..9889cc2 --- /dev/null +++ b/packaging/CreateDevCert.ps1 @@ -0,0 +1,44 @@ +#Requires -Version 5.1 +# 자체 서명 코드 사이닝 인증서 생성 + PFX export. +# Subject가 Package.appxmanifest 의 와 정확히 일치해야 한다. + +[CmdletBinding()] +param( + [string]$Subject = 'CN=EverythingToJpegDev', + [string]$OutputPfx = (Join-Path $PSScriptRoot 'EverythingToJpeg-DevCert.pfx'), + [securestring]$Password +) + +$ErrorActionPreference = 'Stop' + +if (-not $Password) { + Write-Host '인증서 PFX 보호용 비밀번호를 입력하세요. (5대 PC에 설치할 때 필요합니다)' + $Password = Read-Host -AsSecureString -Prompt '비밀번호' +} + +Write-Host "Creating self-signed code-signing certificate: $Subject" +$cert = New-SelfSignedCertificate ` + -Type CodeSigningCert ` + -Subject $Subject ` + -KeyAlgorithm RSA ` + -KeyLength 3072 ` + -Provider 'Microsoft Enhanced RSA and AES Cryptographic Provider' ` + -KeyExportPolicy Exportable ` + -KeyUsage DigitalSignature ` + -CertStoreLocation 'Cert:\CurrentUser\My' ` + -HashAlgorithm SHA256 ` + -NotAfter (Get-Date).AddYears(5) ` + -FriendlyName 'EverythingToJpeg Dev' + +Write-Host "Thumbprint: $($cert.Thumbprint)" +Write-Host "Exporting PFX: $OutputPfx" +Export-PfxCertificate -Cert $cert -FilePath $OutputPfx -Password $Password | Out-Null + +Write-Host '' +Write-Host '--- 다음 단계 ---' +Write-Host " 1. 이 PFX 파일을 5대 PC 각각에 복사" +Write-Host " 2. 각 PC에서 관리자 PowerShell로:" +Write-Host ' Import-PfxCertificate -CertStoreLocation "Cert:\LocalMachine\TrustedPeople" -FilePath <경로>.pfx -Password (Read-Host -AsSecureString)' +Write-Host ' 3. MSIX 빌드 시 BuildMsix.ps1 -CertThumbprint ' + $cert.Thumbprint +Write-Host '' +Write-Host "PFX는 비밀이므로 절대 git에 커밋하지 마세요. (.gitignore에 *.pfx 추가됨)" diff --git a/packaging/GenerateAssets.ps1 b/packaging/GenerateAssets.ps1 new file mode 100644 index 0000000..6813a8f --- /dev/null +++ b/packaging/GenerateAssets.ps1 @@ -0,0 +1,56 @@ +#Requires -Version 5.1 +# packaging/Assets/ 의 placeholder 아이콘들을 생성한다. +# 추후 진짜 로고로 교체. + +$ErrorActionPreference = 'Stop' +Add-Type -AssemblyName System.Drawing + +$assetsDir = Join-Path $PSScriptRoot 'Assets' +if (-not (Test-Path $assetsDir)) { New-Item -ItemType Directory -Path $assetsDir | Out-Null } + +function New-LogoPng { + param( + [int]$Width, + [int]$Height, + [string]$Path, + [string]$Label = '' + ) + $bmp = New-Object System.Drawing.Bitmap($Width, $Height, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb) + $g = [System.Drawing.Graphics]::FromImage($bmp) + $g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias + $g.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::ClearTypeGridFit + + $rect = New-Object System.Drawing.Rectangle(0, 0, $Width, $Height) + $brush = New-Object System.Drawing.Drawing2D.LinearGradientBrush( + $rect, + [System.Drawing.Color]::FromArgb(0xFF, 0x3B, 0x82, 0xF6), + [System.Drawing.Color]::FromArgb(0xFF, 0x1E, 0x40, 0xAF), + [System.Drawing.Drawing2D.LinearGradientMode]::Diagonal) + $g.FillRectangle($brush, $rect) + + if ($Label) { + $fontSize = [Math]::Max(8, [Math]::Min($Width, $Height) / 5) + $font = New-Object System.Drawing.Font('Segoe UI', $fontSize, [System.Drawing.FontStyle]::Bold) + $textBrush = [System.Drawing.Brushes]::White + $sf = New-Object System.Drawing.StringFormat + $sf.Alignment = [System.Drawing.StringAlignment]::Center + $sf.LineAlignment = [System.Drawing.StringAlignment]::Center + $rectF = New-Object System.Drawing.RectangleF(0, 0, [float]$Width, [float]$Height) + $g.DrawString($Label, $font, $textBrush, $rectF, $sf) + $font.Dispose() + $sf.Dispose() + } + + $g.Dispose() + $bmp.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png) + $bmp.Dispose() + $brush.Dispose() + Write-Host " [+] $Path ($Width x $Height)" +} + +Write-Host 'Generating placeholder logos…' +New-LogoPng -Width 50 -Height 50 -Path (Join-Path $assetsDir 'StoreLogo.png') -Label 'E2J' +New-LogoPng -Width 44 -Height 44 -Path (Join-Path $assetsDir 'Square44x44Logo.png') -Label 'E2J' +New-LogoPng -Width 150 -Height 150 -Path (Join-Path $assetsDir 'Square150x150Logo.png')-Label 'E2J' +New-LogoPng -Width 310 -Height 150 -Path (Join-Path $assetsDir 'Wide310x150Logo.png') -Label 'EverythingToJpeg' +Write-Host 'Done.' diff --git a/packaging/Install-EverythingToJpeg.ps1 b/packaging/Install-EverythingToJpeg.ps1 new file mode 100644 index 0000000..74518c8 --- /dev/null +++ b/packaging/Install-EverythingToJpeg.ps1 @@ -0,0 +1,41 @@ +#Requires -Version 5.1 +#Requires -RunAsAdministrator +# 5대 PC에서 MSIX 사이드로드 설치 — 1회 셋업 스크립트. +# 사용법: +# PowerShell (관리자) > .\Install-EverythingToJpeg.ps1 -PfxPath .\EverythingToJpeg-DevCert.pfx -MsixPath .\EverythingToJpeg.msix + +[CmdletBinding()] +param( + [Parameter(Mandatory)] [string]$PfxPath, + [Parameter(Mandatory)] [string]$MsixPath, + [securestring]$PfxPassword +) + +$ErrorActionPreference = 'Stop' + +if (-not (Test-Path $PfxPath)) { throw "PFX 파일을 찾을 수 없습니다: $PfxPath" } +if (-not (Test-Path $MsixPath)) { throw "MSIX 파일을 찾을 수 없습니다: $MsixPath" } + +if (-not $PfxPassword) { + $PfxPassword = Read-Host -AsSecureString -Prompt 'PFX 비밀번호' +} + +Write-Host '[1/3] 인증서를 LocalMachine\TrustedPeople에 임포트…' +$importResult = Import-PfxCertificate ` + -CertStoreLocation 'Cert:\LocalMachine\TrustedPeople' ` + -FilePath $PfxPath ` + -Password $PfxPassword +Write-Host " Thumbprint: $($importResult.Thumbprint)" + +Write-Host '[2/3] 인증서를 LocalMachine\Root에도 임포트 (체인 신뢰)…' +Import-PfxCertificate ` + -CertStoreLocation 'Cert:\LocalMachine\Root' ` + -FilePath $PfxPath ` + -Password $PfxPassword | Out-Null + +Write-Host '[3/3] MSIX 패키지 설치…' +Add-AppxPackage -Path $MsixPath -ForceApplicationShutdown + +Write-Host '' +Write-Host '✅ 설치 완료. Win11 메인 우클릭 메뉴에 "JPEG로 빠른 변환" / "JPEG로 변환…" 항목이 보일 겁니다.' +Write-Host ' (탐색기 재시작이 필요할 수 있음: 작업 관리자 → "Windows 탐색기" 다시 시작)' diff --git a/packaging/Package.appxmanifest b/packaging/Package.appxmanifest new file mode 100644 index 0000000..91c4cdb --- /dev/null +++ b/packaging/Package.appxmanifest @@ -0,0 +1,182 @@ + + + + + + + EverythingToJpeg + YunChan + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/README.md b/packaging/README.md index 7b7eb2e..92b9e26 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -1,25 +1,87 @@ -# Phase 2 — MSIX 패키징 (placeholder) +# Phase 2 — MSIX 패키징 -이 폴더는 향후 IExplorerCommand 셸 익스텐션 + MSIX Sparse Package 작업을 위한 자리입니다. 현재 구현되지 않았습니다. +Win11 메인 우클릭 메뉴에 "JPEG로 빠른 변환" / "JPEG로 변환…"을 띄우는 정공법. -## 다음 단계 체크리스트 +## 구성 -- [ ] `EverythingToJpeg.Shell` C++/WinRT 또는 C#(WinRT projection) 프로젝트 생성 → `IExplorerCommand` 구현 -- [ ] `Package.appxmanifest` 작성 — `` 사용 -- [ ] Windows Application Packaging Project (.wapproj) 생성 — App + Shell DLL 묶기 -- [ ] 자체 서명 인증서 생성 스크립트: - ```powershell - New-SelfSignedCertificate -Type CodeSigningCert ` - -Subject "CN=EverythingToJpegDev" ` - -KeyAlgorithm RSA -KeyLength 2048 ` - -CertStoreLocation "Cert:\CurrentUser\My" - ``` -- [ ] MakeAppx + SignTool로 MSIX 빌드 + 서명 -- [ ] 5대 PC에 인증서를 `Cert:\LocalMachine\TrustedPeople`에 임포트 -- [ ] GitHub Actions: 태그 푸시 시 unsigned MSIX 자동 빌드 + Release 첨부 +``` +packaging/ +├── Package.appxmanifest — IExplorerCommand 등록 (com:Class + desktop4:FileExplorerContextMenus) +├── Assets/ — 앱 아이콘 (placeholder, GenerateAssets.ps1로 생성) +├── GenerateAssets.ps1 — placeholder PNG 일괄 생성 +├── CreateDevCert.ps1 — 자체 서명 코드사이닝 인증서 생성 + PFX export +├── BuildMsix.ps1 — .NET publish + C++ DLL 빌드 + makeappx + (선택) signtool +└── Install-EverythingToJpeg.ps1 — 5대 PC 1회 설치 스크립트 +``` -## 참고 +C++ Shell DLL은 `src/EverythingToJpeg.Shell/` 에 있고 `BuildMsix.ps1` 안에서 자동 빌드됩니다. -- [PowerToys 컨텍스트 메뉴 개발 문서](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/common/context-menus.md) -- [IExplorerCommand C# 예제](https://github.com/cjee21/IExplorerCommand-Examples) -- [Microsoft: Sparse package 등록](https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps) +## 1회: 자체 서명 인증서 만들기 + +```powershell +cd packaging +.\CreateDevCert.ps1 +# Subject 기본값: CN=EverythingToJpegDev (Package.appxmanifest의 Publisher와 일치) +# 비밀번호 입력 → EverythingToJpeg-DevCert.pfx 생성 +``` + +출력된 Thumbprint를 `BuildMsix.ps1 -CertThumbprint <값>` 으로 사용하거나, PFX 파일을 5대 PC에 복사해서 설치 시 사용합니다. + +## 빌드 + +### 미서명 (Phase 1 그대로 사용 가능, 메인 메뉴 노출은 안 됨) +```powershell +.\BuildMsix.ps1 +# 산출: packaging/dist/EverythingToJpeg-x64.msix +``` + +### 서명 +```powershell +# 방법 1: PFX 사용 +.\BuildMsix.ps1 -Sign -PfxPath .\EverythingToJpeg-DevCert.pfx + +# 방법 2: 인증서 저장소의 Thumbprint +.\BuildMsix.ps1 -Sign -CertThumbprint AABBCCDD... +``` + +## 5대 PC 설치 (관리자 PowerShell) + +```powershell +.\Install-EverythingToJpeg.ps1 ` + -PfxPath .\EverythingToJpeg-DevCert.pfx ` + -MsixPath .\EverythingToJpeg-x64.msix +``` + +스크립트가 자동으로: +1. PFX를 `LocalMachine\TrustedPeople` 에 임포트 +2. PFX를 `LocalMachine\Root` 에도 임포트 (체인 신뢰) +3. `Add-AppxPackage` 로 MSIX 사이드로드 + +설치 후 PNG/JPG/HEIC/PDF/DOCX 등을 우클릭하면 **메인 메뉴에 직접** "JPEG로 빠른 변환" / "JPEG로 변환…"이 보입니다. + +## 미서명 사이드로드 (Phase 2 임시 사용) + +자체 서명 만들기조차 귀찮을 때: +```powershell +# 개발자 모드 켜기: 설정 → 개인 정보 및 보안 → 개발자용 → 켜기 +Add-AppxPackage -AllowUnsigned -Path .\EverythingToJpeg-x64.msix +``` +> Win11 24H2부터 `-AllowUnsigned` 지원. 이전 버전은 자체 서명 권장. + +## CI/CD + +`.github/workflows/release.yml` — 태그 푸시(`v1.0.0` 등) 시 자동: +1. .NET / MSBuild 셋업 +2. `BuildMsix.ps1` 실행 (미서명) +3. GitHub Release 생성 + MSIX 첨부 + +서명까지 자동화하려면 GitHub Secrets에 `PFX_BASE64`, `PFX_PASSWORD`를 등록하고 워크플로에 단계 추가 (별도 보안 검토 후). + +## 트러블슈팅 + +| 증상 | 원인 / 해결 | +|---|---| +| `Add-AppxPackage`: "신뢰할 수 없는 인증서" | PFX를 `LocalMachine\TrustedPeople`에 임포트했는지 확인 (Install 스크립트 자동 수행) | +| 메뉴가 안 뜸 | 탐색기 재시작: 작업관리자 → "Windows 탐색기" 다시 시작 | +| Publisher 불일치 오류 | `Package.appxmanifest`의 `Publisher=` 와 인증서 `Subject` 가 정확히 일치해야 함 | +| `App identity required` | MSIX 패키지로 설치된 경우에만 IExplorerCommand 작동. portable EXE는 Phase 1 레지스트리 방식 사용 | diff --git a/src/EverythingToJpeg.Shell/EverythingToJpeg.Shell.vcxproj b/src/EverythingToJpeg.Shell/EverythingToJpeg.Shell.vcxproj new file mode 100644 index 0000000..c340920 --- /dev/null +++ b/src/EverythingToJpeg.Shell/EverythingToJpeg.Shell.vcxproj @@ -0,0 +1,110 @@ + + + + + Debug + x64 + + + Release + x64 + + + Release + ARM64 + + + + + 17.0 + Win32Proj + {1A2B3C4D-5E6F-7A8B-9C0D-EF1234567890} + EverythingToJpegShell + 10.0 + + + + + + DynamicLibrary + true + v145 + Unicode + + + DynamicLibrary + false + v145 + true + Unicode + + + + + + + + + + + Level4 + true + true + Use + pch.h + stdcpp20 + EVERYTHINGTOJPEG_SHELL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + /utf-8 %(AdditionalOptions) + + + Windows + true + Source.def + + + + + + _DEBUG;%(PreprocessorDefinitions) + + + + + + true + true + MultiThreaded + Guard + NDEBUG;%(PreprocessorDefinitions) + + + true + true + true + + + + + + + + + + + + Create + + + + + + + + + + + + + diff --git a/src/EverythingToJpeg.Shell/Source.def b/src/EverythingToJpeg.Shell/Source.def new file mode 100644 index 0000000..51dbd24 --- /dev/null +++ b/src/EverythingToJpeg.Shell/Source.def @@ -0,0 +1,5 @@ +LIBRARY + +EXPORTS + DllGetClassObject PRIVATE + DllCanUnloadNow PRIVATE diff --git a/src/EverythingToJpeg.Shell/dllmain.cpp b/src/EverythingToJpeg.Shell/dllmain.cpp new file mode 100644 index 0000000..228df55 --- /dev/null +++ b/src/EverythingToJpeg.Shell/dllmain.cpp @@ -0,0 +1,188 @@ +// EverythingToJpeg shell extension — IExplorerCommand handlers +// Two verbs: +// - QuickCommandHandler → "EverythingToJpeg.exe quick """ +// - DialogCommandHandler → "EverythingToJpeg.exe dialog """ + +#include "pch.h" + +#pragma warning(disable : 4324) + +using Microsoft::WRL::ClassicCom; +using Microsoft::WRL::ComPtr; +using Microsoft::WRL::InhibitRoOriginateError; +using Microsoft::WRL::Module; +using Microsoft::WRL::ModuleType; +using Microsoft::WRL::RuntimeClass; +using Microsoft::WRL::RuntimeClassFlags; + +namespace { + +constexpr const wchar_t* kExeFileName = L"EverythingToJpeg.exe"; + +std::wstring QuoteForCommandLineArg(const std::wstring& arg) { + const std::wstring quotable_chars(L" \\\""); + if (arg.find_first_of(quotable_chars) == std::wstring::npos) { + return arg; + } + + std::wstring out; + out.push_back(L'"'); + for (size_t i = 0; i < arg.size(); ++i) { + if (arg[i] == L'\\') { + const size_t start = i; + size_t end = start + 1; + for (; end < arg.size() && arg[end] == L'\\'; ++end) {} + size_t backslash_count = end - start; + if (end == arg.size() || arg[end] == L'"') { + backslash_count *= 2; + } + for (size_t j = 0; j < backslash_count; ++j) + out.push_back(L'\\'); + i = end - 1; + } + else if (arg[i] == L'"') { + out.push_back(L'\\'); + out.push_back(L'"'); + } + else { + out.push_back(arg[i]); + } + } + out.push_back(L'"'); + return out; +} + +std::filesystem::path ResolveExePath() { + std::filesystem::path module_path{ + wil::GetModuleFileNameW(wil::GetModuleInstanceHandle()) }; + module_path = module_path.remove_filename(); + module_path /= kExeFileName; + return module_path; +} + +HRESULT LaunchAppWithItems(const wchar_t* verb, IShellItemArray* items) { + if (!items) return S_OK; + + DWORD count = 0; + RETURN_IF_FAILED(items->GetCount(&count)); + if (count == 0) return S_OK; + + auto exe_path = ResolveExePath(); + + auto command = wil::str_printf(LR"-("%s" %s)-", + exe_path.c_str(), verb); + + for (DWORD i = 0; i < count; ++i) { + ComPtr item; + if (FAILED(items->GetItemAt(i, &item))) continue; + + wil::unique_cotaskmem_string path; + if (FAILED(item->GetDisplayName(SIGDN_FILESYSPATH, &path))) continue; + + command = wil::str_printf(LR"-(%s %s)-", + command.c_str(), + QuoteForCommandLineArg(path.get()).c_str()); + } + + wil::unique_process_information process_info; + STARTUPINFOW startup_info = { sizeof(startup_info) }; + RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW( + nullptr, + command.data(), + nullptr, + nullptr, + FALSE, + CREATE_NO_WINDOW, + nullptr, + nullptr, + &startup_info, + &process_info)); + + return S_OK; +} + +template +class CommandHandlerBase : public RuntimeClass< + RuntimeClassFlags, + IExplorerCommand> +{ +public: + IFACEMETHODIMP GetTitle(IShellItemArray*, PWSTR* name) override { + return SHStrDupW(Derived::Title(), name); + } + + IFACEMETHODIMP GetIcon(IShellItemArray*, PWSTR* icon) override { + auto exe = ResolveExePath(); + return SHStrDupW(exe.c_str(), icon); + } + + IFACEMETHODIMP GetToolTip(IShellItemArray*, PWSTR* infoTip) override { + *infoTip = nullptr; + return E_NOTIMPL; + } + + IFACEMETHODIMP GetCanonicalName(GUID* guidCommandName) override { + *guidCommandName = GUID_NULL; + return S_OK; + } + + IFACEMETHODIMP GetState(IShellItemArray*, BOOL, EXPCMDSTATE* cmdState) override { + *cmdState = ECS_ENABLED; + return S_OK; + } + + IFACEMETHODIMP GetFlags(EXPCMDFLAGS* flags) override { + *flags = ECF_DEFAULT; + return S_OK; + } + + IFACEMETHODIMP EnumSubCommands(IEnumExplorerCommand** enumCommands) override { + *enumCommands = nullptr; + return E_NOTIMPL; + } + + IFACEMETHODIMP Invoke(IShellItemArray* items, IBindCtx*) override { + return LaunchAppWithItems(Derived::Verb(), items); + } +}; + +} // namespace + +class __declspec(uuid("801B2DD3-632C-4731-9510-AEAE09345264")) + QuickCommandHandler final + : public CommandHandlerBase +{ +public: + static constexpr const wchar_t* Title() { return L"JPEG로 빠른 변환"; } + static constexpr const wchar_t* Verb() { return L"quick"; } +}; + +class __declspec(uuid("CEBA1DB7-9175-4DF6-A362-490DEA49B598")) + DialogCommandHandler final + : public CommandHandlerBase +{ +public: + static constexpr const wchar_t* Title() { return L"JPEG로 변환…"; } + static constexpr const wchar_t* Verb() { return L"dialog"; } +}; + +CoCreatableClass(QuickCommandHandler) +CoCreatableClass(DialogCommandHandler) +CoCreatableClassWrlCreatorMapInclude(QuickCommandHandler) +CoCreatableClassWrlCreatorMapInclude(DialogCommandHandler) + +BOOL APIENTRY DllMain(HMODULE, DWORD, LPVOID) { + return TRUE; +} + +_Check_return_ +STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) { + if (ppv == nullptr) return E_POINTER; + *ppv = nullptr; + return Module::GetModule().GetClassObject(rclsid, riid, ppv); +} + +__control_entrypoint(DllExport) +STDAPI DllCanUnloadNow(void) { + return Module::GetModule().GetObjectCount() == 0 ? S_OK : S_FALSE; +} diff --git a/src/EverythingToJpeg.Shell/framework.h b/src/EverythingToJpeg.Shell/framework.h new file mode 100644 index 0000000..5cb4cbf --- /dev/null +++ b/src/EverythingToJpeg.Shell/framework.h @@ -0,0 +1,4 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include diff --git a/src/EverythingToJpeg.Shell/pch.cpp b/src/EverythingToJpeg.Shell/pch.cpp new file mode 100644 index 0000000..1d9f38c --- /dev/null +++ b/src/EverythingToJpeg.Shell/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/src/EverythingToJpeg.Shell/pch.h b/src/EverythingToJpeg.Shell/pch.h new file mode 100644 index 0000000..0eab913 --- /dev/null +++ b/src/EverythingToJpeg.Shell/pch.h @@ -0,0 +1,24 @@ +#ifndef PCH_H +#define PCH_H + +#include "framework.h" + +#include +#include + +#include +#include +#pragma comment(lib, "shlwapi.lib") + +#include +#include +#include +#pragma comment(lib, "runtimeobject.lib") + +#pragma warning(push) +#pragma warning(disable: 28182) +#include +#include +#pragma warning(pop) + +#endif