投稿日:2025年2月23日
Adobe LightroomはRAWデータを現像するソフトです。
写真を撮る人には必須なアイテムです。
Lightroomの現像結果のパラメータは、同じ数値でしかコピペできません。
タイムラプスを撮っていると、少々不便に感じます。
日没や日の出の撮影では、明暗の変化に伴い、明るい側、暗い側で異なるパラメータを適用することが多々あります。

▲明るい側の設定。露光量をやや上げ、コントラストも上げています。

▲暗い側の設定。暗さを強調するため露光量を下げ、コントラストもさらに上げています。
現像結果をコピーし、他の画像に適用します。

▲現像した画像を右クリックで現像設定 > 設定をコピーを選択します。

▲項目を確認してコピーをクリックします。

▲全ての画像を選択し設定をペーストしても同じ数値が反映されます。
例えば、最初の1枚目に露光量を+100。
最後の画像に露光量-100。
という数値を適用し、
2枚目を+99。
3枚目を+98。
4枚目、+97。
……略
97枚目、-97。
98枚目、-98。
99枚目、-99。
100枚目を-100。
というように数値変化のグラデーションを持たせたいとします。
数値変化のグラデーションを線形補間と言います。
ChatGPTに、線形補間を記述するPHPプログラムを書いてもらいました。
ファイルのアップロード、ダウンロードが伴い少々危険なので、実際に動くデモサイトは公開しません。
ソースコードだけ記述しておきます。
自己責任にてご検証ください。
以下になります。
index.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>パラメータ線形補間</title>
<script>
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("files").addEventListener("change", function(event) {
const input = event.target;
const fileList = Array.from(input.files);
// `.DS_Store` を除外
const filteredFiles = fileList.filter(file => file.name !== ".DS_Store");
// `.DS_Store` を除外したリストを `input.files` にセット
const dataTransfer = new DataTransfer();
filteredFiles.forEach(file => dataTransfer.items.add(file));
input.files = dataTransfer.files;
console.log("選択されたファイル数(.DS_Store 除外後):", input.files.length);
});
});
</script>
</head>
<body>
<form action="process.php" method="post" enctype="multipart/form-data">
<label for="files">XMPが入ったフォルダを選択してください。</label><br>
<input type="file" id="files" name="files[]" webkitdirectory directory multiple><br>
<button type="submit" style="margin-top:.5rem;">送信</button>
</form>
</body>
</html>
▲xmpファイル選択の画面です。
process.php
<?php
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_FILES['files'])) {
$files = $_FILES['files'];
$uploadBaseDir = 'uploads/'; // 保存先フォルダ
if (!is_dir($uploadBaseDir)) {
mkdir($uploadBaseDir, 0777, true);
}
$validFiles = [];
$leafFolders = []; // 最下層フォルダを記録
$crsAttributes = [
"Temperature", // 露光量
"Tint", // 色かぶり補正
"Exposure2012", // 露光量
"Contrast2012", // コントラスト
"Highlights2012", // ハイライト
"Shadows2012", // シャドウ
"Whites2012", // 白レベル
"Blacks2012", // 黒レベル
"Texture", // テクスチャ
"Clarity2012", // 明瞭
"Dehaze", // かすみの除去
"Vibrance", // 自然な再度
"Saturation", // 彩度
// ▼ カラーミキサー(ミキサー)
"HueAdjustmentRed", // 色相 レッド
"HueAdjustmentOrange", // 色相 オレンジ
"HueAdjustmentYellow", // 色相 イエロー
"HueAdjustmentGreen", // 色相 グリーン
"HueAdjustmentAqua", // 色相 アクア
"HueAdjustmentBlue", // 色相 ブルー
"HueAdjustmentPurple", // 色相 パープル
"HueAdjustmentMagenta", // 色相 マゼンタ
"SaturationAdjustmentRed", // 彩度 レッド
"SaturationAdjustmentOrange", // 彩度 オレンジ
"SaturationAdjustmentYellow", // 彩度 イエロー
"SaturationAdjustmentGreen", // 彩度 グリーン
"SaturationAdjustmentAqua", // 彩度 アクア
"SaturationAdjustmentBlue", // 彩度 ブルー
"SaturationAdjustmentPurple", // 彩度 パープル
"SaturationAdjustmentMagenta", // 彩度 マゼンタ
"LuminanceAdjustmentRed", // 輝度 レッド
"LuminanceAdjustmentOrange", // 輝度 オレンジ
"LuminanceAdjustmentYellow", // 輝度 イエロー
"LuminanceAdjustmentGreen", // 輝度 グリーン
"LuminanceAdjustmentAqua", // 輝度 アクア
"LuminanceAdjustmentBlue", // 輝度 ブルー
"LuminanceAdjustmentPurple", // 輝度 パープル
"LuminanceAdjustmentMagenta" // 輝度 マゼンタ
];
// ファイルをアップロードし、元のフォルダ構造を維持
foreach ($files['name'] as $key => $name) {
if ($files['error'][$key] === UPLOAD_ERR_OK && $name !== '.DS_Store') {
$tmpName = $files['tmp_name'][$key];
$relativePath = isset($files['full_path'][$key]) ? $files['full_path'][$key] : $name;
$savePath = $uploadBaseDir . $relativePath;
// 必要ならフォルダを作成
$saveDir = dirname($savePath);
if (!is_dir($saveDir)) {
mkdir($saveDir, 0777, true);
}
// 最下層のフォルダを記録(ZIP 用)
if (!in_array($saveDir, $leafFolders)) {
$leafFolders[] = $saveDir;
}
// ファイルを保存
if (move_uploaded_file($tmpName, $savePath)) {
$validFiles[] = ['name' => $name, 'path' => $savePath];
}
}
}
// ファイル名でソート
usort($validFiles, function ($a, $b) {
return strcmp($a['path'], $b['path']);
});
// 各 `crs:` 属性の最初と最後の値を取得
$firstValues = [];
$lastValues = [];
if (!empty($validFiles)) {
$firstFileContent = file_get_contents($validFiles[0]['path']);
$lastFileContent = file_get_contents($validFiles[count($validFiles) - 1]['path']);
foreach ($crsAttributes as $attr) {
if (preg_match('/crs:' . $attr . '="([^"]+)"/', $firstFileContent, $matches)) {
$firstValues[$attr] = $matches[1]; // 文字列のまま取得
}
if (preg_match('/crs:' . $attr . '="([^"]+)"/', $lastFileContent, $matches)) {
$lastValues[$attr] = $matches[1]; // 文字列のまま取得
}
}
}
// 補間して書き換え
if (!empty($firstValues) && !empty($lastValues) && count($validFiles) > 1) {
foreach ($validFiles as $index => $file) {
$fileContent = file_get_contents($file['path']);
foreach ($crsAttributes as $attr) {
if (isset($firstValues[$attr]) && isset($lastValues[$attr])) {
$newValue = $firstValues[$attr]; // デフォルトは最初の値
// 数値として処理可能なら補間する
if (is_numeric($firstValues[$attr]) && is_numeric($lastValues[$attr])) {
$difference = ($lastValues[$attr] - $firstValues[$attr]) / (count($validFiles) - 1);
$newValue = $firstValues[$attr] + ($difference * $index);
// **プラスの場合は "+" を付ける**
if ($newValue > 0) {
$newValue = '+' . $newValue;
}
}
// `crs:Attribute=""` の値を更新
$fileContent = preg_replace('/(crs:' . $attr . '=")[^"]+(")/', '${1}' . $newValue . '${2}', $fileContent);
}
}
// 上書き保存
file_put_contents($file['path'], $fileContent);
}
} else {
echo '有効なファイルが少なすぎます。<br>';
}
// ZIP 作成
$zipFileName = 'uploads/download.zip';
$zip = new ZipArchive();
if ($zip->open($zipFileName, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
die('ZIP ファイルの作成に失敗しました。');
}
foreach ($leafFolders as $folderPath) {
$folderName = basename($folderPath);
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($folderPath, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($files as $file) {
$filePath = $file->getRealPath();
$relativePath = $folderName . '/' . basename($filePath);
$zip->addFile($filePath, $relativePath);
}
}
if (!$zip->close()) {
die('ZIP ファイルの保存に失敗しました。');
}
// ZIP 作成後、アップロードされたフォルダを削除
foreach ($leafFolders as $folderPath) {
exec("rm -rf " . escapeshellarg($folderPath)); // フォルダごと削除(Linux / macOS)
}
if (!file_exists($zipFileName)) {
die('ZIP ファイルが正しく作成されていません。');
}
echo '<h2>アップロード & 処理完了!</h2>';
echo '<a href="download.php" class="btn">ZIP をダウンロード</a>';
}
▲ファイル処理を行い、処理したファイルをディレクトリごとZIP圧縮します。
ダウンロード機能も実装されてます。
14〜51行目が線形補間対象のXMPに記述されている属性になります。
XMPとはRAWデータが所持しているLightroomのメタデータです。
これが全ての属性ではなく、私が現像時によく調整する属性です。
download.php
<?php
$zipFileName = 'uploads/download.zip';
if (!file_exists($zipFileName)) {
die("ZIP ファイルが見つかりません。");
}
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="download.zip"');
header('Content-Length: ' . filesize($zipFileName));
readfile($zipFileName);
unlink($zipFileName);
exit;
▲zipダウンロード後、ファイルを削除するので、ファイルが見つからなかった場合に表示するページになります。
処理工程です。
①XMPの出力

▲下部のフィルムストリップから対象のファイルを選択し、右クリック > メタデータ > メタデータをファイルに保存を選択します。

▲RAWデータ(Canonの場合、拡張子は.CR3)が格納されている同ディレクトリに出力されます。
以下はXMPの内容です。
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 7.0-c000 1.000000, 0000/00/00-00:00:00 ">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:tiff="http://ns.adobe.com/tiff/1.0/"
xmlns:exif="http://ns.adobe.com/exif/1.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:aux="http://ns.adobe.com/exif/1.0/aux/"
xmlns:exifEX="http://cipa.jp/exif/1.0/"
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"
xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#"
xmlns:crd="http://ns.adobe.com/camera-raw-defaults/1.0/"
xmlns:xmpDM="http://ns.adobe.com/xmp/1.0/DynamicMedia/"
xmlns:crs="http://ns.adobe.com/camera-raw-settings/1.0/"
xmp:ModifyDate="2025-02-22T16:50:02.51+09:00"
xmp:CreateDate="2025-02-22T16:50:02.51+09:00"
xmp:MetadataDate="2025-02-22T19:23:59+09:00"
tiff:Make="Canon"
tiff:Model="Canon EOS RP"
tiff:Orientation="1"
tiff:ImageWidth="6240"
tiff:ImageLength="4160"
exif:ExifVersion="0231"
exif:ExposureTime="1/2500"
exif:ShutterSpeedValue="11287712/1000000"
exif:FNumber="56/10"
exif:ApertureValue="4970854/1000000"
exif:ExposureProgram="3"
exif:SensitivityType="2"
exif:RecommendedExposureIndex="400"
exif:ExposureBiasValue="0/1"
exif:MaxApertureValue="3/1"
exif:MeteringMode="5"
exif:FocalLength="14/1"
exif:CustomRendered="0"
exif:ExposureMode="0"
exif:WhiteBalance="1"
exif:SceneCaptureType="0"
exif:FocalPlaneXResolution="56907257/32768"
exif:FocalPlaneYResolution="56907257/32768"
exif:FocalPlaneResolutionUnit="3"
exif:DateTimeOriginal="2025-02-22T16:50:02.51+09:00"
exif:PixelXDimension="6240"
exif:PixelYDimension="4160"
dc:format="image/x-canon-cr3"
aux:SerialNumber="341028000079"
aux:LensInfo="14/1 24/1 0/0 0/0"
aux:Lens="14-24mm F2.8 DG HSM | Art 018"
aux:LensID="368"
aux:LensSerialNumber=""
aux:ImageNumber="0"
aux:ApproximateFocusDistance="4294967295/1"
aux:FlashCompensation="0/1"
aux:Firmware="1.6.0"
exifEX:LensModel="14-24mm F2.8 DG HSM | Art 018"
photoshop:DateCreated="2025-02-22T16:50:02.51+09:00"
photoshop:SidecarForExtension="CR3"
photoshop:EmbeddedXMPDigest=""
xmpMM:DocumentID=""
xmpMM:PreservedFileName="_MG_1671.CR3"
xmpMM:OriginalDocumentID=""
xmpMM:InstanceID="xmp.iid:"
crd:CameraProfile="Camera Faithful"
crd:LookName=""
xmpDM:pick="0"
crs:Version="17.2"
crs:ProcessVersion="15.4"
crs:WhiteBalance="As Shot"
crs:Temperature="+5650"
crs:Tint="+15"
crs:Exposure2012="+1"
crs:Contrast2012="+22"
crs:Highlights2012="-100"
crs:Shadows2012="+11"
crs:Whites2012="-100"
crs:Blacks2012="+6"
crs:Texture="0"
crs:Clarity2012="+20"
crs:Dehaze="+9"
crs:Vibrance="+23"
crs:Saturation="+6"
crs:ParametricShadows="0"
crs:ParametricDarks="0"
crs:ParametricLights="0"
crs:ParametricHighlights="0"
crs:ParametricShadowSplit="25"
crs:ParametricMidtoneSplit="50"
crs:ParametricHighlightSplit="75"
crs:Sharpness="40"
crs:SharpenRadius="+1.0"
crs:SharpenDetail="25"
crs:SharpenEdgeMasking="0"
crs:LuminanceSmoothing="0"
crs:ColorNoiseReduction="25"
crs:ColorNoiseReductionDetail="50"
crs:ColorNoiseReductionSmoothness="50"
crs:HueAdjustmentRed="0"
crs:HueAdjustmentOrange="+14"
crs:HueAdjustmentYellow="+15"
crs:HueAdjustmentGreen="0"
crs:HueAdjustmentAqua="0"
crs:HueAdjustmentBlue="0"
crs:HueAdjustmentPurple="0"
crs:HueAdjustmentMagenta="0"
crs:SaturationAdjustmentRed="0"
crs:SaturationAdjustmentOrange="0"
crs:SaturationAdjustmentYellow="0"
crs:SaturationAdjustmentGreen="0"
crs:SaturationAdjustmentAqua="0"
crs:SaturationAdjustmentBlue="+13"
crs:SaturationAdjustmentPurple="0"
crs:SaturationAdjustmentMagenta="0"
crs:LuminanceAdjustmentRed="0"
crs:LuminanceAdjustmentOrange="0"
crs:LuminanceAdjustmentYellow="0"
crs:LuminanceAdjustmentGreen="0"
crs:LuminanceAdjustmentAqua="0"
crs:LuminanceAdjustmentBlue="-8"
crs:LuminanceAdjustmentPurple="0"
crs:LuminanceAdjustmentMagenta="0"
crs:SplitToningShadowHue="0"
crs:SplitToningShadowSaturation="0"
crs:SplitToningHighlightHue="0"
crs:SplitToningHighlightSaturation="0"
crs:SplitToningBalance="0"
crs:ColorGradeMidtoneHue="0"
crs:ColorGradeMidtoneSat="0"
crs:ColorGradeShadowLum="0"
crs:ColorGradeMidtoneLum="0"
crs:ColorGradeHighlightLum="0"
crs:ColorGradeBlending="50"
crs:ColorGradeGlobalHue="0"
crs:ColorGradeGlobalSat="0"
crs:ColorGradeGlobalLum="0"
crs:AutoLateralCA="1"
crs:LensProfileEnable="1"
crs:LensManualDistortionAmount="0"
crs:VignetteAmount="0"
crs:DefringePurpleAmount="0"
crs:DefringePurpleHueLo="30"
crs:DefringePurpleHueHi="70"
crs:DefringeGreenAmount="0"
crs:DefringeGreenHueLo="40"
crs:DefringeGreenHueHi="60"
crs:PerspectiveUpright="0"
crs:PerspectiveVertical="0"
crs:PerspectiveHorizontal="0"
crs:PerspectiveRotate="0.0"
crs:PerspectiveAspect="0"
crs:PerspectiveScale="100"
crs:PerspectiveX="0.00"
crs:PerspectiveY="0.00"
crs:GrainAmount="0"
crs:PostCropVignetteAmount="0"
crs:ShadowTint="0"
crs:RedHue="0"
crs:RedSaturation="0"
crs:GreenHue="0"
crs:GreenSaturation="0"
crs:BlueHue="0"
crs:BlueSaturation="0"
crs:HDREditMode="0"
crs:SDRBrightness="+23"
crs:SDRContrast="-16"
crs:SDRClarity="+3"
crs:SDRHighlights="+100"
crs:SDRShadows="+21"
crs:SDRWhites="+46"
crs:SDRBlend="+18"
crs:OverrideLookVignette="False"
crs:ToneCurveName2012="Linear"
crs:CameraProfile="Adobe Standard"
crs:CameraProfileDigest=""
crs:LensProfileSetup="Custom"
crs:LensProfileName="Adobe (SIGMA 14-24mm F2.8 DG HSM A018, Canon) v2"
crs:LensProfileFilename="Canon (SIGMA 14-24mm F2.8 DG HSM A018) v2 - RAW.lcp"
crs:LensProfileDigest=""
crs:LensProfileIsEmbedded="False"
crs:LensProfileDistortionScale="100"
crs:LensProfileVignettingScale="101"
crs:HasSettings="True"
crs:AlreadyApplied="False"
crs:RawFileName="_MG_1671.CR3">
<exif:ISOSpeedRatings>
<rdf:Seq>
<rdf:li>400</rdf:li>
</rdf:Seq>
</exif:ISOSpeedRatings>
<exif:Flash
exif:Fired="False"
exif:Return="0"
exif:Mode="0"
exif:Function="False"
exif:RedEyeMode="False"/>
<dc:creator>
<rdf:Seq>
<rdf:li>osawa kazushi</rdf:li>
</rdf:Seq>
</dc:creator>
<xmpMM:History>
<rdf:Seq>
<rdf:li
stEvt:action="saved"
stEvt:instanceID="xmp.iid:"
stEvt:when="2025-02-22T19:23:59+09:00"
stEvt:softwareAgent="Adobe Photoshop Lightroom Classic 14.2 (Macintosh)"
stEvt:changed="/metadata"/>
</rdf:Seq>
</xmpMM:History>
<crs:ToneCurvePV2012>
<rdf:Seq>
<rdf:li>0, 0</rdf:li>
<rdf:li>255, 255</rdf:li>
</rdf:Seq>
</crs:ToneCurvePV2012>
<crs:ToneCurvePV2012Red>
<rdf:Seq>
<rdf:li>0, 0</rdf:li>
<rdf:li>255, 255</rdf:li>
</rdf:Seq>
</crs:ToneCurvePV2012Red>
<crs:ToneCurvePV2012Green>
<rdf:Seq>
<rdf:li>0, 0</rdf:li>
<rdf:li>255, 255</rdf:li>
</rdf:Seq>
</crs:ToneCurvePV2012Green>
<crs:ToneCurvePV2012Blue>
<rdf:Seq>
<rdf:li>0, 0</rdf:li>
<rdf:li>255, 255</rdf:li>
</rdf:Seq>
</crs:ToneCurvePV2012Blue>
<crs:PointColors>
<rdf:Seq>
<rdf:li>-1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000</rdf:li>
</rdf:Seq>
</crs:PointColors>
<crs:Look>
<rdf:Description
crs:Name="Adobe Color"
crs:Amount="1"
crs:UUID=""
crs:SupportsAmount="false"
crs:SupportsMonochrome="false"
crs:SupportsOutputReferred="false"
crs:Copyright="© 2018 Adobe Systems, Inc.">
<crs:Group>
<rdf:Alt>
<rdf:li xml:lang="x-default">Profiles</rdf:li>
</rdf:Alt>
</crs:Group>
<crs:Parameters>
<rdf:Description
crs:Version="17.2"
crs:ProcessVersion="15.4"
crs:ConvertToGrayscale="False"
crs:CameraProfile="Adobe Standard"
crs:LookTable="">
<crs:ToneCurvePV2012>
<rdf:Seq>
<rdf:li>0, 0</rdf:li>
<rdf:li>22, 16</rdf:li>
<rdf:li>40, 35</rdf:li>
<rdf:li>127, 127</rdf:li>
<rdf:li>224, 230</rdf:li>
<rdf:li>240, 246</rdf:li>
<rdf:li>255, 255</rdf:li>
</rdf:Seq>
</crs:ToneCurvePV2012>
<crs:ToneCurvePV2012Red>
<rdf:Seq>
<rdf:li>0, 0</rdf:li>
<rdf:li>255, 255</rdf:li>
</rdf:Seq>
</crs:ToneCurvePV2012Red>
<crs:ToneCurvePV2012Green>
<rdf:Seq>
<rdf:li>0, 0</rdf:li>
<rdf:li>255, 255</rdf:li>
</rdf:Seq>
</crs:ToneCurvePV2012Green>
<crs:ToneCurvePV2012Blue>
<rdf:Seq>
<rdf:li>0, 0</rdf:li>
<rdf:li>255, 255</rdf:li>
</rdf:Seq>
</crs:ToneCurvePV2012Blue>
</rdf:Description>
</crs:Parameters>
</rdf:Description>
</crs:Look>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
▲カメラやレンズ、撮影時のデータなど詳細に記述されています。
67〜184行目のcrs:で始まる属性が現像設定値になります。
②線形補間処理

▲出力したXMPを処理するディレクトリに移動します。

▲今回はローカル環境での使用とします。
http://localhost/index.phpにアクセスします。
※上記のプログラムではサーバーへのアップロードでエラーが出ます。

▲ファイル選択をクリックするとディレクトリを選択するダイアログが開くので、先ほどxmpを格納したフォルダを選択します。

▲Macの不可視ファイル、.DS_Storeまで含めるので101個と表示されてしまいます。
ブラウザのデフォルト表示になるので、これは修正できませんでした。

▲こちらの表示はできたので、100と表示されます。
送信ボタンをクリックします。

▲処理後の画面。
ディレクトリごとZIP圧縮する仕様にしました。
ZIPをダウンロードします。

▲任意ディレクトリに保存します。

▲ZIP解凍後、XMPをLightroom RAWデータのあるフォルダへ移動します。

▲メタデータに変更があった場合、フィルムストリップのサムネールに上向きの矢印が表示されます。
全ファイルを選択し、右クリック > メタデータ > メタデータからファイルを読み込むを選択します。
全ファイルに線形補間したデータが反映されます。
今回テストした画像は全部で100枚あります。
その内、処理後の1〜4枚目。97〜100枚目のパラメータは以下になります。

▲1枚目のパラメータ。

▲2枚目のパラメータ。

▲3枚目のパラメータ。

▲4枚目のパラメータ。

▲97枚目のパラメータ。

▲98枚目のパラメータ。

▲99枚目のパラメータ。

▲100枚目のパラメータ。
とすべて線形補間された結果が反映されているのが分かります。
線形補間を利用し、作成したタイムラプスをYouTubeに公開してあります。
進むにつれて滑らかに暗くなっていくのが分かると思います。
今回は全てChatGPTに作ってもらいました。
数値の変化点は最初と最後だけでなく、中間点にも設定して、
最初〜途中〜最後
といった補間もお願いしましたが、何回やっても上手くいかない始末。。。
結局、この状態で共有することにしました。
おまけにサーバーにアップして試してみましたが、アップロードで上手くいかず、ZIPが生成されません。
まだまだ改善の余地があります。
以降、フォルダをドラッグ&ドロップでアップロードしたり、カスタマイズするパラメータをチェックボックスで扱えるようにしたいです。
最後までお読みいただき、ありがとうございます。