Photoshopで疑似網点生成するjsx

Photoshopに疑似AM網点を生成させます。
アクションでできるのですが、網角と線数を都度設定したくて。

![SS_Adobe Photoshop 2025_20250616-151130.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35477/8a44bddb-b8c9-4018-a3b0-5384a771e2d8.png)

90%GPTコードです。
https://d.pr/f/qXn80v

# 使い方
何でも良いんですが、
Illustratorデータを網点化してみましょう。

:::note info
PhotoshopはIllustratorPDFの完璧なラスタライズはできません。Finder(QuickLook)やPreview.appと同程度の解釈しかできず、Acrobat（＝印刷）と同じにはならないこともあります。
:::
▼Photoshopで読み込むのはPDF部分なので、PDF互換ファイルを付けてください
![SS_Finder_20250616-150028.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35477/3b706318-fee4-4420-a7e6-68632c0d7862.png)

aiファイルをPhotoshopに読ませると、ラスタライズ設定が出ます。
▼アンチエイリアスオフ、2,400ppiのCMYKでラスタライズさせてください。
![SS_Finder_20250616-150055.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35477/e4a54206-10c1-45a2-a035-4f4180fbc4bc.png)

▼開いたら、スクリプトを実行。プリセットにK45とM45、7度振りがあります。
![SS_Finder_20250616-150122.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35477/fdb2dd68-7152-4020-b729-e31f9383073f.png)

網点化されます。
Photoshopの2階調ハーフトーンスクリーンはあくまでもPhotoshopの網点ですので、商業印刷で同じ形状にはなりませんし、モアレの確認などもってのほかですが、学習用としては使えます。

▼画像統合の上、チャンネル選択して全チャンネルの目玉を表示状態にしてから……
![SS_Finder_20250616-150805.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35477/bde2ce31-dbe8-4afb-93c4-0d9dc716c87c.png)

▼移動ツールでチャンネルごとにずらせば、版ズレのシミュレーションもできます。
![名称未設定-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35477/74696f7d-3704-44d7-931b-5f50cce95496.png)

以下コード


```javascript
/*
<javascriptresource>
<name>疑似網点生成CMYK</name>
<category>YPresets</category>
</javascriptresource>
*/

(function () {

var keys = ["C", "M", "Y", "K"];
var presetNames = ["A: K45度_175線", "B: M45度_175線", "C: 7度_175線"];
var presets = [
  { C: [15, 175], M: [75, 175], Y: [0, 175], K: [45, 175] },
  { C: [15, 175], M: [45, 175], Y: [0, 175], K: [75, 175] },
  { C: [22, 175], M: [52, 175], Y: [7, 190], K: [82, 175] }
];

function buildDialog() {
  var dlg = new Window("dialog", "線数と角度の設定");
  dlg.orientation = "column";
  dlg.alignChildren = "left";

  var presetList = dlg.add("dropdownlist", undefined, presetNames);
  presetList.selection = 0;

  var inputGroup = dlg.add("group");
  inputGroup.orientation = "column";
  var inputFields = {};

  for (var i = 0; i < keys.length; i++) {
    var row = inputGroup.add("group");
    row.add("statictext", undefined, keys[i] + ": 角度");
    inputFields[keys[i]] = {};
    inputFields[keys[i]].angle = row.add("edittext", undefined, "0");
    inputFields[keys[i]].angle.characters = 5;
    row.add("statictext", undefined, "線数");
    inputFields[keys[i]].lpi = row.add("edittext", undefined, "0");
    inputFields[keys[i]].lpi.characters = 5;
  }

  var resGroup = dlg.add("group");
  resGroup.add("statictext", undefined, "出力解像度:");
  var resDropdown = resGroup.add("dropdownlist", undefined, ["4800", "2400", "1200", "600"]);
  resDropdown.selection = 1;
  inputFields.resolution = resDropdown;

  presetList.onChange = function () {
    var sel = presets[presetList.selection.index];
    for (var i = 0; i < keys.length; i++) {
      inputFields[keys[i]].angle.text = sel[keys[i]][0];
      inputFields[keys[i]].lpi.text = sel[keys[i]][1];
    }
  };
  presetList.notify();
  dlg.add("group").add("button", undefined, "OK");

  return dlg.show() == 1 ? inputFields : null;
}

function getScreenSettings(fields) {
  var s = {};
  for (var i = 0; i < keys.length; i++) {
    s[keys[i]] = {
      angle: parseFloat(fields[keys[i]].angle.text),
      lpi: parseFloat(fields[keys[i]].lpi.text)
    };
  }
  s.resolution = parseInt(fields.resolution.selection.text, 10);
  return s;
}

function binarizeChannel(doc, setting, resolution) {
  app.activeDocument = doc;
  // doc.resizeImage(undefined, undefined, resolution, ResampleMethod.NONE); // 解像度変更を無効化
  var bmo = new BitmapConversionOptions();
  bmo.resolution = resolution;
  bmo.method = BitmapConversionType.HALFTONESCREEN;
  bmo.frequency = setting.lpi;
  bmo.angle = setting.angle;
  bmo.shape = BitmapHalfToneType.ROUND;
  doc.changeMode(ChangeMode.BITMAP, bmo);
  doc.changeMode(ChangeMode.GRAYSCALE);
}

function mergeCMYKChannels(docNames) {
  var desc = new ActionDescriptor();
  var list = new ActionList();
  for (var i = 0; i < 4; i++) {
    var ref = new ActionReference();
    ref.putName(stringIDToTypeID("document"), docNames[i]);
    list.putReference(ref);
  }
  desc.putList(stringIDToTypeID("null"), list);
  desc.putEnumerated(stringIDToTypeID("mode"), stringIDToTypeID("colorSpace"), stringIDToTypeID("CMYKColorEnum"));
  try {
    executeAction(stringIDToTypeID("mergeChannels"), desc, DialogModes.NO);
  } catch (e) {
    alert("mergeChannels に失敗しました\n" + e);
  }
}

if (app.documents.length === 0 || app.activeDocument.mode !== DocumentMode.CMYK) {
  alert("CMYKモードの画像を開いてから実行してください。");
  return;
}

var inputFields = buildDialog();
if (!inputFields) return;

var screenSettings = getScreenSettings(inputFields);
var resolution = screenSettings.resolution;
var doc = app.activeDocument;
var dup = doc.duplicate(doc.name.replace(/\.[^\.]+$/, "") + "_複製", true);
try {
  dup.flatten();
} catch (e) {
  // 統合済みで flatten が不要な場合はスキップ
}
app.activeDocument = dup;
var splitDocs = dup.splitChannels();

var binarizedDocs = [];
for (var i = 0; i < keys.length; i++) {
  binarizeChannel(splitDocs[i], screenSettings[keys[i]], resolution);
  binarizedDocs.push(splitDocs[i]);
}

var docNames = [];
for (var i = 0; i < binarizedDocs.length; i++) {
  docNames.push(binarizedDocs[i].name);
}
mergeCMYKChannels(docNames);

})();
```