Phase 2: MSIX 패키지 + IExplorerCommand 셸 익스텐션
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) <noreply@anthropic.com>
This commit is contained in:
parent
8fa4a613d7
commit
f2c8610ff6
19 changed files with 973 additions and 41 deletions
55
.github/workflows/release.yml
vendored
Normal file
55
.github/workflows/release.yml
vendored
Normal file
|
|
@ -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`에 신뢰 등록해야 설치됩니다.
|
||||||
27
.gitignore
vendored
27
.gitignore
vendored
|
|
@ -19,3 +19,30 @@ Thumbs.db
|
||||||
|
|
||||||
# rider
|
# rider
|
||||||
.idea/
|
.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/
|
||||||
|
|
|
||||||
37
README.md
37
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 타입 램프)
|
- UI: **WPF-UI 4.3** (Win11 Fluent 2 — Mica 백드롭, Segoe UI Variable 타입 램프)
|
||||||
- 변환 엔진: Magick.NET, PDFtoImage, PhotoSauce.MagicScaler + Libheif
|
- 변환 엔진: Magick.NET, PDFtoImage, PhotoSauce.MagicScaler + Libheif
|
||||||
|
|
||||||
## 로드맵 (Phase 2)
|
## 로드맵
|
||||||
|
|
||||||
1. **IExplorerCommand 셸 익스텐션 + MSIX Sparse Package** — Win11 메인 컨텍스트 메뉴 직접 노출
|
| 단계 | 상태 | 내용 |
|
||||||
2. **HTML 변환** — WebView2 헤드리스, viewport 옵션
|
|---|---|---|
|
||||||
3. **HWP/HWPX 변환** — LibreOffice + H2Orestart 자동 설치 가이드
|
| Phase 1 | ✅ | 레지스트리 컨텍스트 메뉴 (Win11 "추가 옵션 표시"), 핵심 변환(이미지·HEIC·RAW·PDF·DOCX), Fluent UI |
|
||||||
4. **GitHub Releases CI/CD** — 태그 푸시 시 자동 빌드 + MSIX 패키징
|
| Phase 2 | ✅ 빌드 가능 | C++ IExplorerCommand DLL, MSIX 패키징, 자체 서명 인증서, GitHub Releases 자동화 — `packaging/README.md` 참조 |
|
||||||
5. **자체 서명 인증서 자동 생성·배포** — 내부 5대 PC 신뢰 체인 자동화 (현재는 unsigned MSIX → 개발자 모드 필요)
|
| 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 (지금 바로 가능)
|
### B) MSIX 패키지 — Win11 메인 메뉴 노출 (Phase 2)
|
||||||
1. `publish` 폴더 통째로 PC에 복사
|
- `packaging/BuildMsix.ps1` 로 MSIX 빌드
|
||||||
2. `EverythingToJpeg.exe` 실행 → 한 번만 "컨텍스트 메뉴 등록"
|
- 자체 서명 인증서를 `LocalMachine\TrustedPeople`에 임포트 후 사이드로드
|
||||||
3. 끝. SmartScreen 경고가 뜨면 "추가 정보" → "실행"
|
- 우클릭 → 바로 메인 메뉴에 항목 노출
|
||||||
|
- 자세한 절차는 [packaging/README.md](packaging/README.md)
|
||||||
### 옵션 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에 임포트
|
|
||||||
|
|
||||||
## 프로젝트 구조
|
## 프로젝트 구조
|
||||||
|
|
||||||
|
|
|
||||||
BIN
packaging/Assets/Square150x150Logo.png
Normal file
BIN
packaging/Assets/Square150x150Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
packaging/Assets/Square44x44Logo.png
Normal file
BIN
packaging/Assets/Square44x44Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 670 B |
BIN
packaging/Assets/StoreLogo.png
Normal file
BIN
packaging/Assets/StoreLogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 729 B |
BIN
packaging/Assets/Wide310x150Logo.png
Normal file
BIN
packaging/Assets/Wide310x150Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
138
packaging/BuildMsix.ps1
Normal file
138
packaging/BuildMsix.ps1
Normal file
|
|
@ -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"
|
||||||
44
packaging/CreateDevCert.ps1
Normal file
44
packaging/CreateDevCert.ps1
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#Requires -Version 5.1
|
||||||
|
# 자체 서명 코드 사이닝 인증서 생성 + PFX export.
|
||||||
|
# Subject가 Package.appxmanifest 의 <Identity Publisher="..."> 와 정확히 일치해야 한다.
|
||||||
|
|
||||||
|
[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 추가됨)"
|
||||||
56
packaging/GenerateAssets.ps1
Normal file
56
packaging/GenerateAssets.ps1
Normal file
|
|
@ -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.'
|
||||||
41
packaging/Install-EverythingToJpeg.ps1
Normal file
41
packaging/Install-EverythingToJpeg.ps1
Normal file
|
|
@ -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 탐색기" 다시 시작)'
|
||||||
182
packaging/Package.appxmanifest
Normal file
182
packaging/Package.appxmanifest
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Package
|
||||||
|
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||||
|
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||||
|
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||||
|
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
|
||||||
|
xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4"
|
||||||
|
xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5"
|
||||||
|
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
|
||||||
|
IgnorableNamespaces="uap rescap desktop desktop4 desktop5 com">
|
||||||
|
|
||||||
|
<Identity
|
||||||
|
Name="EverythingToJpeg.YunChan"
|
||||||
|
Publisher="CN=EverythingToJpegDev"
|
||||||
|
Version="1.0.0.0"
|
||||||
|
ProcessorArchitecture="x64" />
|
||||||
|
|
||||||
|
<Properties>
|
||||||
|
<DisplayName>EverythingToJpeg</DisplayName>
|
||||||
|
<PublisherDisplayName>YunChan</PublisherDisplayName>
|
||||||
|
<Logo>Assets\StoreLogo.png</Logo>
|
||||||
|
</Properties>
|
||||||
|
|
||||||
|
<Dependencies>
|
||||||
|
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
|
||||||
|
</Dependencies>
|
||||||
|
|
||||||
|
<Resources>
|
||||||
|
<Resource Language="ko-KR" />
|
||||||
|
<Resource Language="en-US" />
|
||||||
|
</Resources>
|
||||||
|
|
||||||
|
<Applications>
|
||||||
|
<Application Id="EverythingToJpeg"
|
||||||
|
Executable="EverythingToJpeg.exe"
|
||||||
|
EntryPoint="Windows.FullTrustApplication">
|
||||||
|
<uap:VisualElements
|
||||||
|
DisplayName="EverythingToJpeg"
|
||||||
|
Description="모든 파일을 JPEG로 변환"
|
||||||
|
BackgroundColor="transparent"
|
||||||
|
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||||
|
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||||
|
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||||
|
</uap:VisualElements>
|
||||||
|
|
||||||
|
<Extensions>
|
||||||
|
<!-- COM Surrogate Server: hosts the two IExplorerCommand handlers -->
|
||||||
|
<com:Extension Category="windows.comServer">
|
||||||
|
<com:ComServer>
|
||||||
|
<com:SurrogateServer DisplayName="EverythingToJpeg Shell Extension">
|
||||||
|
<com:Class Id="801B2DD3-632C-4731-9510-AEAE09345264"
|
||||||
|
Path="EverythingToJpeg.Shell.dll"
|
||||||
|
ThreadingModel="STA" />
|
||||||
|
<com:Class Id="CEBA1DB7-9175-4DF6-A362-490DEA49B598"
|
||||||
|
Path="EverythingToJpeg.Shell.dll"
|
||||||
|
ThreadingModel="STA" />
|
||||||
|
</com:SurrogateServer>
|
||||||
|
</com:ComServer>
|
||||||
|
</com:Extension>
|
||||||
|
|
||||||
|
<!-- File Explorer context menu registrations -->
|
||||||
|
<desktop4:Extension Category="windows.fileExplorerContextMenus">
|
||||||
|
<desktop4:FileExplorerContextMenus>
|
||||||
|
<!-- 이미지 -->
|
||||||
|
<desktop5:ItemType Type=".png">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".jpg">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".jpeg">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".gif">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".bmp">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".tif">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".tiff">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".webp">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".avif">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".psd">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
|
||||||
|
<!-- HEIC/HEIF -->
|
||||||
|
<desktop5:ItemType Type=".heic">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".heif">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
|
||||||
|
<!-- RAW -->
|
||||||
|
<desktop5:ItemType Type=".dng">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".nef">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".cr2">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".cr3">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".arw">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".raf">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".orf">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".rw2">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".srw">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".pef">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
|
||||||
|
<!-- PDF -->
|
||||||
|
<desktop5:ItemType Type=".pdf">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
|
||||||
|
<!-- DOCX -->
|
||||||
|
<desktop5:ItemType Type=".docx">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
<desktop5:ItemType Type=".doc">
|
||||||
|
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
|
||||||
|
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
|
||||||
|
</desktop5:ItemType>
|
||||||
|
</desktop4:FileExplorerContextMenus>
|
||||||
|
</desktop4:Extension>
|
||||||
|
</Extensions>
|
||||||
|
</Application>
|
||||||
|
</Applications>
|
||||||
|
|
||||||
|
<Capabilities>
|
||||||
|
<rescap:Capability Name="runFullTrust" />
|
||||||
|
</Capabilities>
|
||||||
|
</Package>
|
||||||
|
|
@ -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` 작성 — `<uap3:Extension Category="windows.fileExplorerContextMenus">` 사용
|
packaging/
|
||||||
- [ ] Windows Application Packaging Project (.wapproj) 생성 — App + Shell DLL 묶기
|
├── Package.appxmanifest — IExplorerCommand 등록 (com:Class + desktop4:FileExplorerContextMenus)
|
||||||
- [ ] 자체 서명 인증서 생성 스크립트:
|
├── Assets/ — 앱 아이콘 (placeholder, GenerateAssets.ps1로 생성)
|
||||||
```powershell
|
├── GenerateAssets.ps1 — placeholder PNG 일괄 생성
|
||||||
New-SelfSignedCertificate -Type CodeSigningCert `
|
├── CreateDevCert.ps1 — 자체 서명 코드사이닝 인증서 생성 + PFX export
|
||||||
-Subject "CN=EverythingToJpegDev" `
|
├── BuildMsix.ps1 — .NET publish + C++ DLL 빌드 + makeappx + (선택) signtool
|
||||||
-KeyAlgorithm RSA -KeyLength 2048 `
|
└── Install-EverythingToJpeg.ps1 — 5대 PC 1회 설치 스크립트
|
||||||
-CertStoreLocation "Cert:\CurrentUser\My"
|
```
|
||||||
```
|
|
||||||
- [ ] MakeAppx + SignTool로 MSIX 빌드 + 서명
|
|
||||||
- [ ] 5대 PC에 인증서를 `Cert:\LocalMachine\TrustedPeople`에 임포트
|
|
||||||
- [ ] GitHub Actions: 태그 푸시 시 unsigned MSIX 자동 빌드 + Release 첨부
|
|
||||||
|
|
||||||
## 참고
|
C++ Shell DLL은 `src/EverythingToJpeg.Shell/` 에 있고 `BuildMsix.ps1` 안에서 자동 빌드됩니다.
|
||||||
|
|
||||||
- [PowerToys 컨텍스트 메뉴 개발 문서](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/common/context-menus.md)
|
## 1회: 자체 서명 인증서 만들기
|
||||||
- [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)
|
```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 레지스트리 방식 사용 |
|
||||||
|
|
|
||||||
110
src/EverythingToJpeg.Shell/EverythingToJpeg.Shell.vcxproj
Normal file
110
src/EverythingToJpeg.Shell/EverythingToJpeg.Shell.vcxproj
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<ItemGroup Label="ProjectConfigurations">
|
||||||
|
<ProjectConfiguration Include="Debug|x64">
|
||||||
|
<Configuration>Debug</Configuration>
|
||||||
|
<Platform>x64</Platform>
|
||||||
|
</ProjectConfiguration>
|
||||||
|
<ProjectConfiguration Include="Release|x64">
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
<Platform>x64</Platform>
|
||||||
|
</ProjectConfiguration>
|
||||||
|
<ProjectConfiguration Include="Release|ARM64">
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
<Platform>ARM64</Platform>
|
||||||
|
</ProjectConfiguration>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Label="Globals">
|
||||||
|
<VCProjectVersion>17.0</VCProjectVersion>
|
||||||
|
<Keyword>Win32Proj</Keyword>
|
||||||
|
<ProjectGuid>{1A2B3C4D-5E6F-7A8B-9C0D-EF1234567890}</ProjectGuid>
|
||||||
|
<RootNamespace>EverythingToJpegShell</RootNamespace>
|
||||||
|
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||||
|
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||||
|
<UseDebugLibraries>true</UseDebugLibraries>
|
||||||
|
<PlatformToolset>v145</PlatformToolset>
|
||||||
|
<CharacterSet>Unicode</CharacterSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||||
|
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||||
|
<UseDebugLibraries>false</UseDebugLibraries>
|
||||||
|
<PlatformToolset>v145</PlatformToolset>
|
||||||
|
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||||
|
<CharacterSet>Unicode</CharacterSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||||
|
|
||||||
|
<ImportGroup Label="PropertySheets">
|
||||||
|
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props"
|
||||||
|
Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')"
|
||||||
|
Label="LocalAppDataPlatform" />
|
||||||
|
</ImportGroup>
|
||||||
|
|
||||||
|
<ItemDefinitionGroup>
|
||||||
|
<ClCompile>
|
||||||
|
<WarningLevel>Level4</WarningLevel>
|
||||||
|
<SDLCheck>true</SDLCheck>
|
||||||
|
<ConformanceMode>true</ConformanceMode>
|
||||||
|
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||||
|
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||||
|
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||||
|
<PreprocessorDefinitions>EVERYTHINGTOJPEG_SHELL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
|
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
|
||||||
|
</ClCompile>
|
||||||
|
<Link>
|
||||||
|
<SubSystem>Windows</SubSystem>
|
||||||
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
|
<ModuleDefinitionFile>Source.def</ModuleDefinitionFile>
|
||||||
|
</Link>
|
||||||
|
</ItemDefinitionGroup>
|
||||||
|
|
||||||
|
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||||
|
<ClCompile>
|
||||||
|
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
|
</ClCompile>
|
||||||
|
</ItemDefinitionGroup>
|
||||||
|
|
||||||
|
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||||
|
<ClCompile>
|
||||||
|
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||||
|
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||||
|
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||||
|
<ControlFlowGuard>Guard</ControlFlowGuard>
|
||||||
|
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
|
</ClCompile>
|
||||||
|
<Link>
|
||||||
|
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||||
|
<OptimizeReferences>true</OptimizeReferences>
|
||||||
|
<CETCompat>true</CETCompat>
|
||||||
|
</Link>
|
||||||
|
</ItemDefinitionGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ClInclude Include="framework.h" />
|
||||||
|
<ClInclude Include="pch.h" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ClCompile Include="dllmain.cpp" />
|
||||||
|
<ClCompile Include="pch.cpp">
|
||||||
|
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||||
|
</ClCompile>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="Source.def" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.260126.7" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||||
|
</Project>
|
||||||
5
src/EverythingToJpeg.Shell/Source.def
Normal file
5
src/EverythingToJpeg.Shell/Source.def
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
LIBRARY
|
||||||
|
|
||||||
|
EXPORTS
|
||||||
|
DllGetClassObject PRIVATE
|
||||||
|
DllCanUnloadNow PRIVATE
|
||||||
188
src/EverythingToJpeg.Shell/dllmain.cpp
Normal file
188
src/EverythingToJpeg.Shell/dllmain.cpp
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
// EverythingToJpeg shell extension — IExplorerCommand handlers
|
||||||
|
// Two verbs:
|
||||||
|
// - QuickCommandHandler → "EverythingToJpeg.exe quick "<paths>""
|
||||||
|
// - DialogCommandHandler → "EverythingToJpeg.exe dialog "<paths>""
|
||||||
|
|
||||||
|
#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<std::wstring>(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<std::wstring>(LR"-("%s" %s)-",
|
||||||
|
exe_path.c_str(), verb);
|
||||||
|
|
||||||
|
for (DWORD i = 0; i < count; ++i) {
|
||||||
|
ComPtr<IShellItem> 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<std::wstring>(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 <typename Derived>
|
||||||
|
class CommandHandlerBase : public RuntimeClass<
|
||||||
|
RuntimeClassFlags<ClassicCom | InhibitRoOriginateError>,
|
||||||
|
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<QuickCommandHandler>
|
||||||
|
{
|
||||||
|
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<DialogCommandHandler>
|
||||||
|
{
|
||||||
|
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<ModuleType::InProc>::GetModule().GetClassObject(rclsid, riid, ppv);
|
||||||
|
}
|
||||||
|
|
||||||
|
__control_entrypoint(DllExport)
|
||||||
|
STDAPI DllCanUnloadNow(void) {
|
||||||
|
return Module<ModuleType::InProc>::GetModule().GetObjectCount() == 0 ? S_OK : S_FALSE;
|
||||||
|
}
|
||||||
4
src/EverythingToJpeg.Shell/framework.h
Normal file
4
src/EverythingToJpeg.Shell/framework.h
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <windows.h>
|
||||||
1
src/EverythingToJpeg.Shell/pch.cpp
Normal file
1
src/EverythingToJpeg.Shell/pch.cpp
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
#include "pch.h"
|
||||||
24
src/EverythingToJpeg.Shell/pch.h
Normal file
24
src/EverythingToJpeg.Shell/pch.h
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#ifndef PCH_H
|
||||||
|
#define PCH_H
|
||||||
|
|
||||||
|
#include "framework.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <shlobj_core.h>
|
||||||
|
#include <shlwapi.h>
|
||||||
|
#pragma comment(lib, "shlwapi.lib")
|
||||||
|
|
||||||
|
#include <wrl/client.h>
|
||||||
|
#include <wrl/implements.h>
|
||||||
|
#include <wrl/module.h>
|
||||||
|
#pragma comment(lib, "runtimeobject.lib")
|
||||||
|
|
||||||
|
#pragma warning(push)
|
||||||
|
#pragma warning(disable: 28182)
|
||||||
|
#include <wil/stl.h>
|
||||||
|
#include <wil/win32_helpers.h>
|
||||||
|
#pragma warning(pop)
|
||||||
|
|
||||||
|
#endif
|
||||||
Loading…
Add table
Add a link
Reference in a new issue