-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgenerate_card.ps1
More file actions
390 lines (337 loc) · 12.5 KB
/
Copy pathgenerate_card.ps1
File metadata and controls
390 lines (337 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
<#
.SYNOPSIS
批量生成 WHUDAYS 社员卡(需要 Adobe Photoshop)
.DESCRIPTION
从 Excel 收集表读取社员信息,使用 Photoshop 自动替换模板中的头像、姓名和部门,
导出每位社员的两个画板预览 PNG。
目录结构:
脚本所在目录/
template.psd (PSD 模板)
generate_card.ps1 (本脚本)
社员卡(收集结果)/ (数据文件夹,名字 = Excel 文件名)
社员卡(收集结果).xlsx
图片/
xxx.jpg ...
.PARAMETER DataFolder
包含 xlsx 和图片文件夹的目录。默认自动查找脚本同目录下的子文件夹。
.PARAMETER OutputDir
输出目录,默认 .\output
.PARAMETER TemplatePath
PSD 模板路径,默认同目录下 template.psd
.PARAMETER CN
仅导出指定 CN 的成员。默认不筛选(导出全部)。
.EXAMPLE
.\generate_card.ps1
.EXAMPLE
.\generate_card.ps1 -DataFolder ".\社员卡(收集结果)"
.EXAMPLE
.\generate_card.ps1 -DataFolder ".\社员卡(收集结果)" -CN "-QuQ-"
#>
param(
[string]$DataFolder,
[string]$OutputDir,
[string]$TemplatePath,
[string]$CN
)
$ErrorActionPreference = "Stop"
# ---------- 路径处理 ----------
if (-not $TemplatePath) {
$TemplatePath = Join-Path $PSScriptRoot "template.psd"
}
if (-not (Test-Path $TemplatePath)) {
Write-Error "找不到模板 PSD:$TemplatePath"
return
}
$TemplatePath = (Resolve-Path $TemplatePath).Path
# ---------- 自动查找数据文件夹 ----------
if (-not $DataFolder) {
$candidates = Get-ChildItem $PSScriptRoot -Directory | Where-Object {
Test-Path (Join-Path $_.FullName "图片")
}
if ($candidates.Count -eq 0) {
Write-Error "未找到包含 图片 子文件夹的数据目录。请用 -DataFolder 参数指定。"
return
}
if ($candidates.Count -gt 1) {
Write-Host "找到多个数据目录:" -ForegroundColor Yellow
for ($i = 0; $i -lt $candidates.Count; $i++) {
Write-Host " [$i] $($candidates[$i].Name)"
}
$sel = Read-Host "请选择 (0-$($candidates.Count - 1))"
$DataFolder = $candidates[[int]$sel].FullName
} else {
$DataFolder = $candidates[0].FullName
}
}
if (-not (Test-Path $DataFolder)) {
Write-Error "数据目录不存在:$DataFolder"
return
}
$DataFolder = (Resolve-Path $DataFolder).Path
# 查找 xlsx 文件
$xlsxFile = Get-ChildItem $DataFolder -Filter "*.xlsx" | Where-Object { $_.Name -notlike '~`$*' } | Select-Object -First 1
if (-not $xlsxFile) {
Write-Error "在 $DataFolder 中未找到 xlsx 文件"
return
}
$xlsxPath = $xlsxFile.FullName
$imageDir = Join-Path $DataFolder "图片"
if (-not (Test-Path $imageDir)) {
Write-Error "图片文件夹不存在:$imageDir"
return
}
Write-Host "Excel: $xlsxPath" -ForegroundColor Gray
Write-Host "图片目录: $imageDir" -ForegroundColor Gray
# ---------- 输出目录 ----------
if (-not $OutputDir) {
$OutputDir = Join-Path $PSScriptRoot "output"
}
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}
$OutputDir = (Resolve-Path $OutputDir).Path
# ---------- 用 PowerShell COM 读取 Excel ----------
$excel = $null
try {
Write-Host "正在读取 Excel..." -ForegroundColor Yellow
$excel = New-Object -ComObject Excel.Application
$excel.Visible = $false
$excel.DisplayAlerts = $false
$wb = $excel.Workbooks.Open($xlsxPath, 0, $true) # ReadOnly
$ws = $wb.Sheets.Item(1)
$maxRow = $ws.UsedRange.Rows.Count
$maxCol = $ws.UsedRange.Columns.Count
# 查找列索引(第1行为表头)
$cnCol = 0
$deptCol = 0
$imgCol = 0
for ($c = 1; $c -le $maxCol; $c++) {
$header = [string]$ws.Cells.Item(1, $c).Value2
if ($header -match 'CN' ) { $cnCol = $c }
elseif ($header -match '部门') { $deptCol = $c }
elseif ($header -match '头像') { $imgCol = $c }
}
if ($cnCol -eq 0 -or $deptCol -eq 0 -or $imgCol -eq 0) {
Write-Error "Excel 表头列未找到: CN列=$cnCol, 部门列=$deptCol, 头像列=$imgCol"
return
}
$members = @()
for ($r = 2; $r -le $maxRow; $r++) {
$cnValue = [string]$ws.Cells.Item($r, $cnCol).Value2
$deptValue = [string]$ws.Cells.Item($r, $deptCol).Value2
$imgValue = [string]$ws.Cells.Item($r, $imgCol).Value2
if (-not $cnValue -or -not $deptValue -or -not $imgValue) { continue }
$imgPath = Join-Path $imageDir $imgValue
if (-not (Test-Path $imgPath)) {
Write-Warning "图片不存在,跳过: $imgValue ($cnValue)"
continue
}
$members += [PSCustomObject]@{
name = $cnValue.Trim()
dept = $deptValue.Trim()
img = (Resolve-Path $imgPath).Path
}
}
$wb.Close($false)
} catch {
Write-Error "读取 Excel 失败: $_"
return
} finally {
if ($excel) {
$excel.Quit()
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($excel) | Out-Null
}
}
if ($members.Count -eq 0) {
Write-Error "Excel 中没有有效记录"
return
}
if ($CN) {
$targetCN = $CN.Trim()
$members = @($members | Where-Object { $_.name -eq $targetCN })
if ($members.Count -eq 0) {
Write-Error "未找到指定 CN:$targetCN"
return
}
}
# ---------- 生成 JSX ----------
function ConvertTo-JsxPath($p) { $p -replace '\\', '/' }
function ConvertTo-JsxStr($s) { $s -replace '\\', '\\\\' -replace '"', '\"' -replace "'", "\'" }
function ConvertTo-SafeFileName($s) { $s -replace '[\\/:*?"<>|]', '_' }
$jsxMembers = ($members | ForEach-Object {
$n = ConvertTo-JsxStr $_.name
$d = ConvertTo-JsxStr $_.dept
$i = ConvertTo-JsxPath $_.img
$safeName = ConvertTo-SafeFileName $_.name
$o1 = ConvertTo-JsxPath (Join-Path $OutputDir "$($safeName)_画板1.png")
$o2 = ConvertTo-JsxPath (Join-Path $OutputDir "$($safeName)_画板2.png")
" { name:`"$n`", dept:`"$d`", img:`"$i`", out1:`"$o1`", out2:`"$o2`" }"
}) -join ",`n"
$jsxTemplate = ConvertTo-JsxPath $TemplatePath
$jsx = @"
// ---- WHUDAYS Card Generator (auto-generated) ----
#target photoshop
app.displayDialogs = DialogModes.NO;
app.preferences.rulerUnits = Units.PIXELS;
function findLayer(parent, name) {
for (var i = 0; i < parent.layers.length; i++) {
var l = parent.layers[i];
if (l.name === name) return l;
if (l.typename === 'LayerSet') {
var f = findLayer(l, name);
if (f) return f;
}
}
return null;
}
function getArtboardRect(layer) {
var ref = new ActionReference();
ref.putIdentifier(stringIDToTypeID('layer'), layer.id);
var desc = executeActionGet(ref);
if (!desc.hasKey(stringIDToTypeID('artboard'))) {
return null;
}
var abDesc = desc.getObjectValue(stringIDToTypeID('artboard'));
if (!abDesc.hasKey(stringIDToTypeID('artboardRect'))) {
return null;
}
var rect = abDesc.getObjectValue(stringIDToTypeID('artboardRect'));
return {
left: rect.getUnitDoubleValue(stringIDToTypeID('left')),
top: rect.getUnitDoubleValue(stringIDToTypeID('top')),
right: rect.getUnitDoubleValue(stringIDToTypeID('right')),
bottom: rect.getUnitDoubleValue(stringIDToTypeID('bottom'))
};
}
function exportArtboard(doc, abName, outPath) {
var ab = findLayer(doc, abName);
if (!ab) { alert('Artboard not found: ' + abName); return; }
// 通过 artboardRect 获取真实画板边界,避免误用内容边界。
var rect = getArtboardRect(ab);
if (!rect) {
alert('Unable to read artboardRect: ' + abName);
return;
}
var bL = rect.left;
var bT = rect.top;
var bR = rect.right;
var bB = rect.bottom;
var w = bR - bL;
var h = bB - bT;
// 在原文档上选中画板区域,复制合并(只含可见像素)
doc.selection.select([
[bL, bT], [bR, bT], [bR, bB], [bL, bB]
]);
doc.selection.copy(true); // copyMerged
doc.selection.deselect();
// 新建透明背景文档,粘贴
var newDoc = app.documents.add(
new UnitValue(w, 'px'),
new UnitValue(h, 'px'),
doc.resolution,
abName,
NewDocumentMode.RGB,
DocumentFill.TRANSPARENT
);
newDoc.paste();
var opts = new PNGSaveOptions();
opts.compression = 0;
opts.interlaced = false;
newDoc.saveAs(new File(outPath), opts, true, Extension.LOWERCASE);
newDoc.close(SaveOptions.DONOTSAVECHANGES);
}
var members = [
$jsxMembers
];
// ---- 逐人导出两个画板(预览模式) ----
for (var m = 0; m < members.length; m++) {
var info = members[m];
var doc = app.open(new File("$jsxTemplate"));
// ---- 替换姓名 ----
var cnLayer = findLayer(doc, 'CN');
if (cnLayer && cnLayer.kind == LayerKind.TEXT) {
cnLayer.textItem.contents = info.name;
}
// ---- 替换部门 ----
var deptLayer = findLayer(doc, '\u90E8\u95E8');
if (deptLayer && deptLayer.kind == LayerKind.TEXT) {
deptLayer.textItem.contents = info.dept;
}
// ---- 替换头像 ----
var avatar = findLayer(doc, '\u5934\u50CF');
if (avatar) {
// 记录原始头像的边界(位置和尺寸)
var origBounds = avatar.bounds;
var origLeft = origBounds[0].as('px');
var origTop = origBounds[1].as('px');
var origRight = origBounds[2].as('px');
var origBottom = origBounds[3].as('px');
var origW = origRight - origLeft;
var origH = origBottom - origTop;
var origCX = origLeft + origW / 2;
var origCY = origTop + origH / 2;
// 替换智能对象内容
doc.activeLayer = avatar;
var desc = new ActionDescriptor();
desc.putPath(charIDToTypeID('null'), new File(info.img));
executeAction(stringIDToTypeID('placedLayerReplaceContents'), desc, DialogModes.NO);
// 获取替换后新尺寸
var newBounds = avatar.bounds;
var newLeft = newBounds[0].as('px');
var newTop = newBounds[1].as('px');
var newRight = newBounds[2].as('px');
var newBottom = newBounds[3].as('px');
var newW = newRight - newLeft;
var newH = newBottom - newTop;
if (newW > 0 && newH > 0) {
// 短边填充:缩放比例取较大值,确保覆盖原始区域
var scaleX = (origW / newW) * 100;
var scaleY = (origH / newH) * 100;
var scale = Math.max(scaleX, scaleY);
// 缩放图层
avatar.resize(scale, scale, AnchorPosition.MIDDLECENTER);
// 重新获取缩放后的边界,计算偏移使中心对齐
var scaledBounds = avatar.bounds;
var scaledCX = (scaledBounds[0].as('px') + scaledBounds[2].as('px')) / 2;
var scaledCY = (scaledBounds[1].as('px') + scaledBounds[3].as('px')) / 2;
var dx = origCX - scaledCX;
var dy = origCY - scaledCY;
avatar.translate(new UnitValue(dx, 'px'), new UnitValue(dy, 'px'));
}
}
exportArtboard(doc, '\u753B\u677F 1', info.out1);
exportArtboard(doc, '\u753B\u677F 2', info.out2);
doc.close(SaveOptions.DONOTSAVECHANGES);
}
"@
$jsxPath = Join-Path $env:TEMP "whudays_card_gen_$(Get-Date -Format 'yyyyMMddHHmmss').jsx"
$utf8Bom = New-Object System.Text.UTF8Encoding $true
[System.IO.File]::WriteAllText($jsxPath, $jsx, $utf8Bom)
# ---------- 调用 Photoshop ----------
Write-Host "`n=== WHUDAYS 社员卡生成器 ===" -ForegroundColor Cyan
Write-Host "模板: $TemplatePath"
Write-Host "数据: $xlsxPath"
Write-Host "输出: $OutputDir"
Write-Host "待处理: $($members.Count) 张卡片`n"
foreach ($m in $members) {
Write-Host " - $($m.name) [$($m.dept)]" -ForegroundColor White
}
Write-Host ""
try {
Write-Host "正在连接 Photoshop..." -ForegroundColor Yellow
$ps = New-Object -ComObject Photoshop.Application
$ps.Visible = $true
Write-Host "正在生成卡片..." -ForegroundColor Yellow
$ps.DoJavascript("$.evalFile('$($jsxPath -replace '\\', '/')')")
Write-Host "`n全部完成!" -ForegroundColor Green
Write-Host "输出文件:"
foreach ($m in $members) {
$safeName = ConvertTo-SafeFileName $m.name
Write-Host " $($safeName)_画板1.png" -ForegroundColor White
Write-Host " $($safeName)_画板2.png" -ForegroundColor White
}
} catch {
Write-Error "Photoshop 脚本执行出错: $_"
} finally {
Remove-Item $jsxPath -ErrorAction SilentlyContinue
}