/** * TINKERINO — Illustrator Template Filler * ======================================= * Bundle dari Tinkerino Web UI berisi: * bundle_folder/ * data.json * illustrator.jsx (file ini) * images/ * main.jpg, layout.gif, exterior.jpg, building1.jpg, shared1.jpg, * floorPlan1.jpg, gallery1.jpg, branch.jpg, accessMap.jpg, ... * * Cara pakai: * 1. Extract ZIP bundle dari Tinkerino di folder manapun. * 2. Buka file .ai template Anda di Illustrator. * 3. File > Scripts > Other Script... → pilih illustrator.jsx ini * 4. Ketika diminta, pilih file data.json di folder bundle. * 5. Script akan: * - Replace isi text frame yang namanya cocok (lihat daftar di bawah) * - Swap PlacedItem (gambar ter-link) yang namanya "image." * dengan file di folder images/ yang cocok. * * ========== PENAMAAN DI ILLUSTRATOR ========== * Di panel LAYERS Illustrator, double-click nama setiap text frame / * placed item, lalu isi sesuai tabel berikut: * * -------- TEKS (nama text frame) -------- * HEADER * propertyName → ブリリアタワー川崎 * propertyNameWithFloor → ブリリアタワー川崎/24階 * floor → 24階 * code → C14262R77 * price → 1億3,980万円 * * LAYOUT * madori → 3LD・K * orientation → 南 * exclusiveArea → 79.97m² * balconyArea → 13.87m² * buildingArea → (kodate) 99.36m² * landArea → (kodate) 132.37m² * floorsInfo → 24階/地上34階 地下1階 * * LOKASI * address → 神奈川県川崎市幸区大宮町28-2 * access → (semua baris, joined " / ") * accessLine1 → 京浜東北・根岸線 「川崎」駅 徒歩6分 * accessLine2 → 京急本線 「京急川崎」駅 徒歩11分 * mapUrl → https://google.com/maps?q=... * * OUTLINE TABLE (物件概要) * rightsType → 所有権 * totalUnits → 395戸 * structure → 鉄筋コンクリート造 * builtYear → 2008年7月 * currentStatus → 居住中 * deliveryDate → 相談 * seller → 東京建物株式会社 他 * constructor → 三井住友建設株式会社 * manager → 株式会社東京建物アメニティサポート * managementType → 全部委託 * managerDuty → 日勤 * managementFee → 23,700円 * repairFee → 20,800円 * otherFee → 990円 * totalFee → 45,490円 * otherFeeDetail → インターネット使用料/990円 * parking → - * transactionType → 媒介(仲介) * remarks → - * * GEDUNG (dari library page) * buildingCatchphrase → ペデストリアンデッキで駅直結... * buildingDescription → 総戸数395戸の大規模マンション... * sellingPoint1..5 → 5 bullet points terpisah * buildingTotalUnits → 395戸 * buildingStructure → RC一部S * buildingScale → 地上34階 地下1階 * buildingYear → 2008年10月 * buildingOwner → 東京建物|ジェイアール東日本都市開発 他 * buildingConstruction → 三井住友建設 * * CABANG * branchName → 川崎センター * branchCompany → 東急リバブル株式会社 川崎センター * branchOpening → 10:00〜18:00 * branchHoliday → 毎週火曜日・水曜日 * branchTel → 044-211-1091 * branchFax → 044-244-1738 * branchFreeCall → 0120-055-109 * branchZip → 210-0007 * branchAddress → 神奈川県川崎市川崎区駅前本町11-1 ... * branchAccess → 東海道本線「川崎」駅 北口東出口徒歩1分 / ... * branchStaff → 間中 (まなか) * branchUrl → https://www.livable.co.jp/branch/kawasaki/ * * META * extractedAt → 2026-04-20 10:56 * sourceUrl → https://www.livable.co.jp/mansion/... * * -------- GAMBAR (nama PlacedItem / gambar ter-link) -------- * Placed item name → file di images/... * image.main → main.jpg (foto utama property) * image.layout → layout.gif/jpg (denah unit) * image.exterior → exterior.jpg (foto luar gedung) * image.building1 → building1.jpg (foto gedung 1) * image.building2..6 → building2..6.jpg * image.shared1..6 → shared1..6.jpg (foto fasilitas bersama) * image.floorPlan1..4 → floorPlan1..4.jpg (denah/contoh interior) * image.gallery1..6 → gallery1..6.jpg (galeri) * image.branch → branch.jpg (foto cabang) * image.accessMap → accessMap.jpg (peta akses) * * Catatan gambar: * - Gambar WAJIB di-Place dengan opsi "Link" (bukan Embed). * File > Place ... → centang "Link" sebelum klik Place. * - Saat script jalan, script akan ganti path link-nya ke file * di folder images/. Hasil akhir tetap LINKED — kalau mau * embed, pilih gambar lalu Object > Embed di Illustrator. * * Tidak cocok? Text frame / placed item tanpa nama yang match * TIDAK akan disentuh. Aman untuk grafis lain yang tidak ingin * diganti. */ #target illustrator (function main() { if (app.documents.length === 0) { alert('Buka file .ai template dulu sebelum menjalankan script.'); return; } var doc = app.activeDocument; // 1) Pilih data.json var jsonFile = File.openDialog('Pilih data.json dari bundle Tinkerino', (File.fs === 'Windows') ? 'JSON:*.json' : undefined); if (!jsonFile) return; if (!jsonFile.exists) { alert('File tidak ditemukan: ' + jsonFile.fsName); return; } jsonFile.encoding = 'UTF-8'; if (!jsonFile.open('r')) { alert('Tidak bisa buka: ' + jsonFile.fsName); return; } var raw = jsonFile.read(); jsonFile.close(); var data; try { data = eval('(' + raw + ')'); } catch (e) { alert('JSON tidak valid:\n' + e.message); return; } // 2) Lokasi folder images/ var bundleDir = jsonFile.parent; var imagesDir = new Folder(bundleDir.fsName + '/images'); var hasImages = imagesDir.exists; // 3) Bangun mapping text var textMap = buildTextMap(data); // 4) Jalankan var result = { textOk: 0, textSkipped: [], imgOk: 0, imgSkipped: [] }; var textFrames = collectTextFrames(doc); for (var i = 0; i < textFrames.length; i++) { var tf = textFrames[i]; var key = normName(tf.name); if (key && textMap.hasOwnProperty(key)) { var val = textMap[key]; if (val !== null && val !== undefined && String(val) !== '') { try { tf.contents = String(val); result.textOk++; } catch (e) { result.textSkipped.push(key + ' (locked?)'); } } } } if (hasImages) { var placedItems = collectPlacedItems(doc); for (var j = 0; j < placedItems.length; j++) { var pi = placedItems[j]; var nm = pi.name; if (!nm) continue; var m = /^image\.([A-Za-z0-9_]+)\s*$/.exec(nm); if (!m) continue; var slot = m[1]; var localFile = findImageFile(imagesDir, slot); if (!localFile) { result.imgSkipped.push(nm + ' (file tidak ada)'); continue; } try { pi.file = localFile; result.imgOk++; } catch (e) { // Embedded image → file property read-only result.imgSkipped.push(nm + ' (embedded / terkunci)'); } } } // 5) Report var msg = 'TINKERINO — Report\n'; msg += '──────────────────────\n'; msg += 'Text frame diganti : ' + result.textOk + '\n'; msg += 'Gambar diganti : ' + result.imgOk + '\n'; if (result.textSkipped.length) { msg += '\nText di-skip:\n ' + uniqJoin(result.textSkipped, '\n '); } if (result.imgSkipped.length) { msg += '\nGambar di-skip:\n ' + uniqJoin(result.imgSkipped, '\n '); } if (!hasImages) { msg += '\n\nFolder images/ tidak ditemukan di sebelah data.json\n'; msg += 'Gambar tidak ada yang diganti.'; } alert(msg); // ==================== HELPERS ==================== function buildTextMap(d) { var b = d.building || {}; var br = d.branch || {}; var al = d.accessLines || []; var sp = b.sellingPoints || []; var coord = d.coordinates || {}; var m = { // Names 'propertyName' : d.propertyName || '', 'propertyNameWithFloor' : d.propertyNameWithFloor || '', 'floor' : d.floor || '', 'code' : d.code || '', // Pricing 'price' : d.price || '', // Layout 'madori' : d.madori || '', 'orientation' : d.orientation || '', 'exclusiveArea' : d.exclusiveArea || '', 'balconyArea' : d.balconyArea || '', 'buildingArea' : d.buildingArea || '', 'landArea' : d.landArea || '', 'floorsInfo' : d.floorsInfo || '', // Location 'address' : d.address || '', 'access' : d.access || '', 'accessLine1' : al[0] || '', 'accessLine2' : al[1] || '', 'accessLine3' : al[2] || '', 'mapUrl' : d.mapUrl || '', 'latitude' : coord.lat != null ? String(coord.lat) : '', 'longitude' : coord.lng != null ? String(coord.lng) : '', // Outline 'rightsType' : d.rightsType || '', 'totalUnits' : d.totalUnits || '', 'structure' : d.structure || '', 'builtYear' : d.builtYear || '', 'currentStatus' : d.currentStatus || '', 'deliveryDate' : d.deliveryDate || '', 'seller' : d.seller || '', 'constructor' : d.constructor || '', 'manager' : d.manager || '', 'managementType' : d.managementType || '', 'managerDuty' : d.managerDuty || '', 'managementFee' : d.managementFee || '', 'repairFee' : d.repairFee || '', 'otherFee' : d.otherFee || '', 'totalFee' : d.totalFee || '', 'otherFeeDetail' : d.otherFeeDetail || '', 'parking' : d.parking || '', 'transactionType' : d.transactionType || '', 'remarks' : d.remarks || '', // Building 'buildingCatchphrase' : b.catchphrase || '', 'buildingDescription' : b.description || '', 'sellingPoint1' : sp[0] || '', 'sellingPoint2' : sp[1] || '', 'sellingPoint3' : sp[2] || '', 'sellingPoint4' : sp[3] || '', 'sellingPoint5' : sp[4] || '', 'buildingTotalUnits' : b.totalUnits || '', 'buildingStructure' : b.structureMaterial || '', 'buildingScale' : b.buildingScale || '', 'buildingYear' : b.buildingYear || '', 'buildingOwner' : b.owner || '', 'buildingConstruction' : b.construction || '', // Branch 'branchName' : br.name || '', 'branchCompany' : br.company || '', 'branchOpening' : br.opening || '', 'branchHoliday' : br.holiday || '', 'branchTel' : br.tel || '', 'branchFax' : br.fax || '', 'branchFreeCall' : br.freeCall || '', 'branchZip' : br.zipCode || '', 'branchAddress' : br.address || '', 'branchAccess' : br.access || '', 'branchStaff' : br.staff || '', 'branchUrl' : br.url || '', // Meta 'extractedAt' : d.extractedAt || '', 'sourceUrl' : d.sourceUrl || '' }; // Total fee fallback: compute kalau tidak ada if (!m.totalFee) { var mf = parseInt((m.managementFee || '').replace(/[^\d]/g, ''), 10) || 0; var rf = parseInt((m.repairFee || '').replace(/[^\d]/g, ''), 10) || 0; var of = parseInt((m.otherFee || '').replace(/[^\d]/g, ''), 10) || 0; var sum = mf + rf + of; if (sum > 0) m.totalFee = formatThousand(sum) + '円'; } return m; } function formatThousand(n) { var s = String(n), out = '', c = 0; for (var i = s.length - 1; i >= 0; i--) { out = s.charAt(i) + out; c++; if (c % 3 === 0 && i > 0) out = ',' + out; } return out; } function normName(nm) { if (!nm) return ''; var s = String(nm); s = s.replace(/^\s*(field|data|text)\s*:\s*/i, ''); return s.replace(/^\s+|\s+$/g, ''); } function findImageFile(folder, slot) { var exts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'JPG', 'PNG', 'GIF']; for (var i = 0; i < exts.length; i++) { var f = new File(folder.fsName + '/' + slot + '.' + exts[i]); if (f.exists) return f; } return null; } function collectTextFrames(container) { var out = []; walk(container.layers, function (it) { if (it.typename === 'TextFrame') out.push(it); }); return out; } function collectPlacedItems(container) { var out = []; walk(container.layers, function (it) { if (it.typename === 'PlacedItem') out.push(it); }); return out; } function walk(items, cb) { for (var i = 0; i < items.length; i++) { var it = items[i]; cb(it); if (it.typename === 'Layer' || it.typename === 'GroupItem') { if (it.pageItems && it.pageItems.length > 0) { walkItems(it.pageItems, cb); } if (it.layers && it.layers.length > 0) { walk(it.layers, cb); } } } } function walkItems(items, cb) { for (var i = 0; i < items.length; i++) { var it = items[i]; cb(it); if (it.typename === 'GroupItem' && it.pageItems && it.pageItems.length > 0) { walkItems(it.pageItems, cb); } } } function uniqJoin(arr, sep) { var seen = {}, out = []; for (var i = 0; i < arr.length; i++) { if (!seen[arr[i]]) { seen[arr[i]] = 1; out.push(arr[i]); } } return out.join(sep); } })();