diff --git a/app/Http/Controllers/Api/Service/VmController.php b/app/Http/Controllers/Api/Service/VmController.php index 16cada2..4c3c8e2 100644 --- a/app/Http/Controllers/Api/Service/VmController.php +++ b/app/Http/Controllers/Api/Service/VmController.php @@ -2,14 +2,13 @@ namespace App\Http\Controllers\Api\Service; -use App\Actions\VM\ChangeIpVMAction; use App\Actions\VM\ChangePortVMAction; use App\Actions\VM\PowerOffVMAction; use App\Actions\VM\PowerOnVMAction; use App\Http\Controllers\Api\Service\Concerns\ResolvesVm; use App\Http\Controllers\Controller; -use App\Http\Services\Telegram; use App\Http\Services\vCenter; +use App\Jobs\VirtualMachine\ChangeIpVMJob; use App\Jobs\VirtualMachine\ChangeVmPasswordJob; use App\Models\ModuleVmware; use App\Models\Os; @@ -19,7 +18,6 @@ use App\Services\VmProvisionService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Log; class VmController extends Controller { @@ -124,7 +122,10 @@ class VmController extends Controller * ip_address string New IP to assign * gateway string (optional) Gateway — falls back to existing IP record */ - public function changeIp(Request $request, ChangeIpVMAction $action): JsonResponse + /** + * Dispatches ChangeIpVMJob — returns immediately, runs async. + */ + public function changeIp(Request $request): JsonResponse { $validated = $request->validate([ 'server_name' => ['required', 'string', 'max:255'], @@ -165,87 +166,35 @@ class VmController extends Controller : 'windows'; $gateway = $validated['gateway'] ?? ''; + $dns = $validated['dns'] ?? '8.8.8.8'; - $dns = $validated['dns'] ?? '8.8.8.8'; - - $lock = $this->acquireVmLock($vm, 120); + // ponytail: don't release — lock expires after 5 min while the async job runs + $lock = $this->acquireVmLock($vm, 300); if (!$lock) { return $this->vmLocked(); } - try { - $raw = $action->execute( - $vm, - $validated['ip_address'], - $gateway, - $osType, - $dns, - $validated['guest_user'], - $validated['guest_password'] - ); - } finally { - $lock->release(); - } - - // Always log the raw PowerShell output for debugging - Log::channel('Custom')->info('[changeIp] raw output', [ - 'server_name' => $validated['server_name'], - 'ip_address' => $validated['ip_address'], - 'gateway' => $gateway, - 'os_type' => $osType, - 'vm_name' => $vm->name, - 'vmware_id' => $vm->vmware_id, - 'raw' => $raw, - ]); - - $telegram = new Telegram(); - - if ($raw === 'Login_Again' || $raw === 'vCenter_Not_Found') { - $errMsg = $raw === 'vCenter_Not_Found' - ? 'Không tìm thấy vCenter cho VM này.' - : 'Không thể đăng nhập vào vCenter.'; - - $telegram->SendErrorMessage("❌ Đổi IP thất bại [{$osType}]\n" - . "🖥 VM: {$vm->name} ({$vm->vmware_id})\n" - . "📍 IP mới: {$validated['ip_address']}\n" - . "🔗 GW: {$gateway}\n" - . "🚫 Lỗi: {$errMsg}"); - - return response()->json(['success' => false, 'message' => $errMsg], 400); - } - - if (str_starts_with((string) $raw, 'Error:') || str_contains((string) $raw, '[STDERR]')) { - $telegram->SendErrorMessage("❌ Đổi IP thất bại [{$osType}]\n" - . "🖥 VM: {$vm->name} ({$vm->vmware_id})\n" - . "📍 IP mới: {$validated['ip_address']}\n" - . "🔗 GW: {$gateway}\n" - . "📋 Output:\n" . mb_substr((string) $raw, 0, 800)); - - return response()->json(['success' => false, 'message' => 'Lệnh đổi IP thất bại. Admin đã được thông báo.', 'data' => ['raw' => $raw]], 400); - } - - // Empty raw = script chạy không có output → coi là thành công (PowerShell script không echo) - // Nhưng vẫn gửi cảnh báo để theo dõi - if (trim((string) $raw) === '') { - $telegram->SendMessage("⚠️ Đổi IP [{$osType}] - Script chạy nhưng không có output\n" - . "🖥 VM: {$vm->name} ({$vm->vmware_id})\n" - . "📍 IP mới: {$validated['ip_address']}\n" - . "🔗 GW: {$gateway}\n" - . "ℹ️ Kiểm tra lại VM để xác nhận IP đã thay đổi chưa."); - } + ChangeIpVMJob::dispatch( + $vm->id, + $validated['ip_address'], + $gateway, + $osType, + $dns, + $validated['guest_user'], + $validated['guest_password'], + ); $this->activityLogger->log( event: 'change_ip', logName: 'VM', - description: "Customer {$request->user()->email} đổi IP VM: {$vm->name} → {$validated['ip_address']}", + description: "Customer {$request->user()->email} yêu cầu đổi IP VM: {$vm->name} → {$validated['ip_address']}", subject: $vm, newValues: ['ip_address' => $validated['ip_address'], 'gateway' => $gateway, 'os_type' => $osType], ); return response()->json([ 'success' => true, - 'message' => 'Đã gửi lệnh đổi IP.', - 'data' => ['raw' => $raw], + 'message' => 'Yêu cầu đổi IP đã được ghi nhận. Hệ thống sẽ xử lý trong vài phút.', ]); } diff --git a/app/Http/Resources/Api/Service/VpsServiceResource.php b/app/Http/Resources/Api/Service/VpsServiceResource.php index ab95447..8fde36f 100644 --- a/app/Http/Resources/Api/Service/VpsServiceResource.php +++ b/app/Http/Resources/Api/Service/VpsServiceResource.php @@ -39,10 +39,8 @@ class VpsServiceResource extends JsonResource $product = $orderItem?->product ?? $this->product; return [ - 'id' => $this->id, 'uuid' => $this->uuid, 'server_name' => $this->server_name, - 'invoice_id' => $this->invoice_id, 'invoice_num' => $invoice?->invoice_number, 'order_item_id' => $this->order_item_id, 'order_num' => $order?->order_num, @@ -56,7 +54,7 @@ class VpsServiceResource extends JsonResource 'group' => $orderItem?->os?->osGroup?->name, ], 'product' => $product ? [ - 'id' => $product->id, + 'uuid' => $product->uuid, 'name' => $product->name, 'slug' => $product->slug, 'product_type' => $product->product_type, diff --git a/app/Http/Services/vCenter/VirtualMachineService.php b/app/Http/Services/vCenter/VirtualMachineService.php index c156f77..83f39e3 100644 --- a/app/Http/Services/vCenter/VirtualMachineService.php +++ b/app/Http/Services/vCenter/VirtualMachineService.php @@ -59,15 +59,16 @@ class VirtualMachineService $userName = customer()?->email; $item = $product_data['item']; + $product = $product_data['product'] ?? null; $host = $product_data['host']; $datacenter = $product_data['datacenter']; $template = $product_data['template']; $addon = $product_data['addon'] ?? ['cpu' => 0, 'ram' => 0, 'disk' => 0]; - $base_cpu = $item->cpu ?? ($host->cpu ?? 1); - $base_ram = $item->ram ?? ($host->ram ?? 1); - $base_disk = $item->disk ?? ($host->disk ?? 20); + $base_cpu = $product->cpu ?? ($host->cpu ?? 1); + $base_ram = $product->ram ?? ($host->ram ?? 1); + $base_disk = $product->disk ?? ($host->disk ?? 20); $base_cpu = max(1, (int) $base_cpu); $base_ram = max(1, (int) $base_ram); diff --git a/app/Jobs/VirtualMachine/ChangeIpVMJob.php b/app/Jobs/VirtualMachine/ChangeIpVMJob.php new file mode 100644 index 0000000..6b113a5 --- /dev/null +++ b/app/Jobs/VirtualMachine/ChangeIpVMJob.php @@ -0,0 +1,120 @@ +find($this->moduleVmwareId); + + if (!$vm) { + Log::error('[ChangeIpVMJob] ModuleVmware not found', ['module_vmware_id' => $this->moduleVmwareId]); + return; + } + + $raw = $action->execute( + $vm, + $this->ipAddress, + $this->gateway, + $this->osType, + $this->dns, + $this->guestUser, + $this->guestPassword, + ); + + Log::channel('Custom')->info('[ChangeIpVMJob] raw output', [ + 'module_vmware_id' => $this->moduleVmwareId, + 'ip_address' => $this->ipAddress, + 'gateway' => $this->gateway, + 'os_type' => $this->osType, + 'vm_name' => $vm->name, + 'vmware_id' => $vm->vmware_id, + 'raw' => $raw, + ]); + + $telegram = new Telegram(); + + if ($raw === 'Login_Again' || $raw === 'vCenter_Not_Found') { + $errMsg = $raw === 'vCenter_Not_Found' + ? 'Không tìm thấy vCenter cho VM này.' + : 'Không thể đăng nhập vào vCenter.'; + + $telegram->SendErrorMessage("❌ Đổi IP thất bại [{$this->osType}]\n" + . "🖥 VM: {$vm->name} ({$vm->vmware_id})\n" + . "📍 IP mới: {$this->ipAddress}\n" + . "🔗 GW: {$this->gateway}\n" + . "🚫 Lỗi: {$errMsg}"); + + return; + } + + if (str_starts_with((string) $raw, 'Error:') || str_contains((string) $raw, '[STDERR]')) { + $telegram->SendErrorMessage("❌ Đổi IP thất bại [{$this->osType}]\n" + . "🖥 VM: {$vm->name} ({$vm->vmware_id})\n" + . "📍 IP mới: {$this->ipAddress}\n" + . "🔗 GW: {$this->gateway}\n" + . "📋 Output:\n" . mb_substr((string) $raw, 0, 800)); + + return; + } + + // Empty raw = script chạy không có output → coi là thành công (PowerShell script không echo) + // Nhưng vẫn gửi cảnh báo để theo dõi + if (trim((string) $raw) === '') { + $telegram->SendMessage("⚠️ Đổi IP [{$this->osType}] - Script chạy nhưng không có output\n" + . "🖥 VM: {$vm->name} ({$vm->vmware_id})\n" + . "📍 IP mới: {$this->ipAddress}\n" + . "🔗 GW: {$this->gateway}\n" + . "ℹ️ Kiểm tra lại VM để xác nhận IP đã thay đổi chưa."); + } + } + + public function failed(\Throwable $e): void + { + Log::error('[ChangeIpVMJob] job failed with exception', [ + 'module_vmware_id' => $this->moduleVmwareId, + 'ip_address' => $this->ipAddress, + 'error' => $e->getMessage(), + ]); + } +} diff --git a/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php b/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php index 7b4dd55..b96f913 100644 --- a/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php +++ b/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Jobs\VirtualMachine\Create; use App\Http\Services\vCenter; -use App\Jobs\VirtualMachine\Provision\AssignIpToGuestJob; +use App\Jobs\VirtualMachine\Provision\UpdateVmConfigJob; use App\Models\OrderItem; use App\Models\VmProvisionLock; use App\Services\VirtualMachine\VmAddonResolver; @@ -13,7 +13,6 @@ use App\Services\VirtualMachine\VmInfrastructureResolver; use App\Services\VirtualMachine\VmIpReservationService; use App\Services\VirtualMachine\VmModuleService; use App\Services\VirtualMachine\VmNotificationService; -use App\Services\VirtualMachine\VmPowerService; use App\Services\VirtualMachine\VmProvisionLogger; use App\Services\VirtualMachine\VmServiceSyncService; use App\Services\VirtualMachine\VmVcenterErrorTranslator; @@ -47,7 +46,6 @@ class CreateVirtualMachineJob implements ShouldQueue VmProvisionLogger $vmLogger, VmInfrastructureResolver $infraResolver, VmAddonResolver $addonResolver, - VmPowerService $powerService, VmModuleService $moduleService, VmServiceSyncService $serviceSync, VmNotificationService $notification, @@ -131,6 +129,7 @@ class CreateVirtualMachineJob implements ShouldQueue $result = $vCenter->CreateVirtualMachine([ 'item' => $this->item, + 'product' => $product, 'host' => $infra->host, 'datacenter' => $infra->datacenter, 'vcenter' => $infra->vcenter, @@ -162,21 +161,6 @@ class CreateVirtualMachineJob implements ShouldQueue 'vm_id' => $vmId, ]); - $vmLogger->step('VM start started', ['vm_id' => $vmId]); - - $startResult = $powerService->start($vCenter, $vmId); - - if (is_array($startResult) && ($startResult['error'] ?? false)) { - throw new RuntimeException( - 'Start VM thất bại: ' . json_encode($startResult['data'] ?? null) - ); - } - if ($startResult === 'Login_Again') { - throw new RuntimeException('Start VM thất bại: cần đăng nhập lại vCenter.'); - } - - $vmLogger->success('VM start success', ['vm_id' => $vmId]); - $moduleVmware = $moduleService->create($this->item, $infra, $vmId); $vmLogger->success('ModuleVmware created', [ @@ -191,16 +175,19 @@ class CreateVirtualMachineJob implements ShouldQueue 'order_item_id' => $this->item->id, ]); - AssignIpToGuestJob::dispatch( + // VM is left powered off after clone. UpdateVmConfigJob owns power-on: + // if the order has a CPU/RAM addon it powers off (no-op, already off), + // resizes, then powers on; otherwise it just powers on. Either way it + // dispatches AssignIpToGuestJob next, which needs the VM running. + UpdateVmConfigJob::dispatch( $moduleVmware->id, (string) $vmId, $infra->isWindows ? VmProvisionService::OS_WINDOWS : VmProvisionService::OS_LINUX, - VmProvisionService::TEMPLATE_PASSWORD, - $reservation->id, - $ipPoolId, + reservationId: $reservation->id, + reservedIpPoolId: $ipPoolId, )->onQueue('default'); - $vmLogger->success('AssignIpToGuestJob dispatched', [ + $vmLogger->success('UpdateVmConfigJob dispatched', [ 'module_vmware_id' => $moduleVmware->id, 'vm_id' => $vmId, 'os_family' => $infra->isWindows ? 'WINDOWS' : 'LINUX', diff --git a/app/Jobs/VirtualMachine/Provision/AssignIpToGuestJob.php b/app/Jobs/VirtualMachine/Provision/AssignIpToGuestJob.php index 36143e4..ddac7f7 100644 --- a/app/Jobs/VirtualMachine/Provision/AssignIpToGuestJob.php +++ b/app/Jobs/VirtualMachine/Provision/AssignIpToGuestJob.php @@ -31,7 +31,8 @@ use Illuminate\Support\Facades\Log; * * Assigns a real static IP to the VM guest OS and verifies it is active. * Only persists the IP to DB (module_vmware + virtual_machines) after - * successful guest-side verification, then dispatches UpdateVmConfigJob → ChangePasswordViaVsphereJob. + * successful guest-side verification, then dispatches ChangePasswordViaVsphereJob. + * CPU/RAM config (UpdateVmConfigJob) already ran before this job, right after clone. * * Flow: * [1] Resolve network config from order item → product → ipAddress template @@ -41,7 +42,7 @@ use Illuminate\Support\Facades\Log; * [5] On FAIL → release IP, try next * [6] Call verifyIPLinux.ps1 / verifyIPWindows.ps1 via SSH * [7] On FAIL → release IP, try next - * [8] SUCCESS → persistIp() → dispatch UpdateVmConfigJob → ChangePasswordViaVsphereJob + * [8] SUCCESS → persistIp() → dispatch ChangePasswordViaVsphereJob * [9] All attempts exhausted → mark VM inactive (status=0) */ class AssignIpToGuestJob implements ShouldQueue @@ -186,7 +187,7 @@ class AssignIpToGuestJob implements ShouldQueue 'attempt' => $attempt, ]); - UpdateVmConfigJob::dispatch( + ChangePasswordViaVsphereJob::dispatch( $this->moduleVmwareId, $this->vmId, $this->osFamily, @@ -228,8 +229,8 @@ class AssignIpToGuestJob implements ShouldQueue 'attempt' => $attempt, ]); - // Dispatch addon config update → then password change - UpdateVmConfigJob::dispatch( + // CPU/RAM config was already applied by UpdateVmConfigJob before IP assignment. + ChangePasswordViaVsphereJob::dispatch( $this->moduleVmwareId, $this->vmId, $this->osFamily, diff --git a/app/Jobs/VirtualMachine/Provision/UpdateVmConfigJob.php b/app/Jobs/VirtualMachine/Provision/UpdateVmConfigJob.php index 490b840..1fcfe00 100644 --- a/app/Jobs/VirtualMachine/Provision/UpdateVmConfigJob.php +++ b/app/Jobs/VirtualMachine/Provision/UpdateVmConfigJob.php @@ -12,6 +12,7 @@ use App\Models\Service; use App\Models\ServiceAddon; use App\Models\vCenter; use App\Services\VirtualMachine\VmProvisionLogger; +use App\Services\VmProvisionService; use App\Services\VsphereHostSshService; use Carbon\Carbon; use Illuminate\Bus\Queueable; @@ -26,22 +27,27 @@ use Illuminate\Support\Facades\Log; * UpdateVmConfigJob * * Applies customer-selected CPU/RAM addons (from addons_meta on the OrderItem) - * to the live vSphere VM via updateVMConfig.ps1. + * to the vSphere VM via updateVMConfig.ps1. Runs right after clone, while the + * VM is still powered off — this is also what CreateVirtualMachineJob relies + * on to power the VM on before the next step. * - * Skipped if the order has no cpu/ram addon → goes straight to ChangePasswordViaVsphereJob. + * Skipped if the order has no cpu/ram addon → just power on, dispatch AssignIpToGuestJob. * * PS1 on vSphere host must accept: * $server_name, $cpu, $memoryGB, $vcenter_host, $vcenter_user, $vcenter_password * and call Connect-VIServer before Get-VM. * - * Flow: + * Flow (order-creation path, reservationId set): * [1] Load ModuleVmware → order → orderItems.first() → product * [2] Resolve addon cpu/ram from addons_meta (code field saved by OrderService) - * [3] If no cpu/ram addon → skip, dispatch ChangePasswordViaVsphereJob + * [3] If no cpu/ram addon → power on, dispatch AssignIpToGuestJob * [4] Load vCenter credentials (same pattern as ChangePasswordViaVsphereJob) * [5] Compute total: base (product.cpu/ram) + addon qty - * [6] SSH → vSphere → updateVMConfig.ps1 with vCenter creds - * [7] Assert SUCCESS → dispatch ChangePasswordViaVsphereJob + * [6] Power off → SSH → vSphere → updateVMConfig.ps1 → power on + * [7] Assert SUCCESS → dispatch AssignIpToGuestJob + * + * Manual-override path (admin panel, reservationId null): same power-off → + * resize → power-on, but only updates DB — no further job is dispatched. */ class UpdateVmConfigJob implements ShouldQueue { @@ -57,6 +63,8 @@ class UpdateVmConfigJob implements ShouldQueue public readonly string $osFamily, public readonly ?int $cpuOverride = null, public readonly ?int $ramOverride = null, + public readonly ?int $reservationId = null, + public readonly ?int $reservedIpPoolId = null, ) {} public function handle(VsphereHostSshService $ssh, VmProvisionLogger $logger, Telegram $telegram): void @@ -91,10 +99,16 @@ class UpdateVmConfigJob implements ShouldQueue [$addonCpu, $addonRamGb] = $this->resolveAddonCpuRam($addonsMeta); if ($addonCpu === 0 && $addonRamGb === 0) { - $logger->step('no cpu/ram addon — skipping UpdateVmConfigJob', [ + $logger->step('no cpu/ram addon — powering on and skipping resize', [ 'module_vmware_id' => $this->moduleVmwareId, 'vm_id' => $this->vmId, ]); + + $vcenterForPowerOn = $moduleVmware->vcenter ?? vCenter::first(); + if ($vcenterForPowerOn) { + $this->powerCycle($vcenterForPowerOn, (string) $moduleVmware->vmware_id, $logger, powerOff: false); + } + $this->nextStep(); return; } @@ -133,9 +147,10 @@ class UpdateVmConfigJob implements ShouldQueue 'total_ram_gb' => $totalRam, ]); - if ($isManualOverride) { - $this->powerCycle($vcenter, (string) $moduleVmware->vmware_id, $logger, powerOff: true); - } + // Power off before resizing (both the create-flow addon path and admin + // manual overrides) — hot CPU/RAM resize isn't reliable across guest + // OSes and is what caused Set-VM to reject the config as invalid. + $this->powerCycle($vcenter, (string) $moduleVmware->vmware_id, $logger, powerOff: true); try { $output = $ssh->updateVmConfig( @@ -147,9 +162,7 @@ class UpdateVmConfigJob implements ShouldQueue $vcenterPass, ); } finally { - if ($isManualOverride) { - $this->powerCycle($vcenter, (string) $moduleVmware->vmware_id, $logger, powerOff: false); - } + $this->powerCycle($vcenter, (string) $moduleVmware->vmware_id, $logger, powerOff: false); } Log::info('UpdateVmConfigJob: PS1 output', [ @@ -273,8 +286,9 @@ class UpdateVmConfigJob implements ShouldQueue /** * Power off/on the VM via vCenter REST and wait (up to 30s) for the state to land. - * Used only for manual admin overrides — hot-add isn't reliable for RAM on every guest OS, - * so we power-cycle around the resize to guarantee it applies. + * Hot-add isn't reliable for CPU/RAM resize on every guest OS, so we + * power-cycle around the resize (and around the power-on-only skip path) + * to guarantee the config applies and the VM ends up running either way. */ private function powerCycle(vCenter $vcenter, string $vmwareId, VmProvisionLogger $logger, bool $powerOff): void { @@ -347,10 +361,13 @@ class UpdateVmConfigJob implements ShouldQueue private function nextStep(): void { - ChangePasswordViaVsphereJob::dispatch( + AssignIpToGuestJob::dispatch( $this->moduleVmwareId, $this->vmId, $this->osFamily, + VmProvisionService::TEMPLATE_PASSWORD, + $this->reservationId, + $this->reservedIpPoolId, ); } } diff --git a/app/Repositories/Api/Invoice/InvoiceRepository.php b/app/Repositories/Api/Invoice/InvoiceRepository.php index 0e5feba..48ec32e 100644 --- a/app/Repositories/Api/Invoice/InvoiceRepository.php +++ b/app/Repositories/Api/Invoice/InvoiceRepository.php @@ -47,7 +47,7 @@ class InvoiceRepository extends BaseApiRepository implements InvoiceRepositoryIn 'tax' => 0, 'total' => $order->total, 'status' => InvoiceStatus::UNPAID->value, - 'due_date' => now()->addDays(7)->toDateString(), + 'due_date' => now()->addMonths($maxMonths)->toDateString(), ]); // Create one invoice_item per order_item line + addon sub-lines diff --git a/resources/views/pdf/web-design-quote.blade.php b/resources/views/pdf/web-design-quote.blade.php index 52e3c1a..3ab5436 100644 --- a/resources/views/pdf/web-design-quote.blade.php +++ b/resources/views/pdf/web-design-quote.blade.php @@ -151,6 +151,9 @@ body { .items-table td.summary-value { text-align: right; font-weight: bold; } .items-table tr.total td { font-size: 12px; font-weight: bold; } .items-table .item-desc { color: #6b7280; font-size: 9px; } +.items-table .item-desc p { margin: 2px 0; } +.items-table .item-desc ul, +.items-table .item-desc ol { margin: 2px 0; padding-left: 16px; } .items-table { position: relative; z-index: 1; } /* Summary */ @@ -316,7 +319,7 @@ ul.ref-list { margin: 2px 0 0 14px; }
|
- Đại diện khách hàng
- (Ký, ghi rõ họ tên)
- |
Đại diện {{ $companyName }}
- (Ký, ghi rõ họ tên)
@if ($quote->creator)
{{ $quote->creator->name }}
@endif
diff --git a/storage/fonts/Times-Roman.afm.json b/storage/fonts/Times-Roman.afm.json
new file mode 100644
index 0000000..c96b2fc
--- /dev/null
+++ b/storage/fonts/Times-Roman.afm.json
@@ -0,0 +1,569 @@
+{
+ "codeToName": {
+ "32": "space",
+ "160": "space",
+ "33": "exclam",
+ "34": "quotedbl",
+ "35": "numbersign",
+ "36": "dollar",
+ "37": "percent",
+ "38": "ampersand",
+ "146": "quoteright",
+ "40": "parenleft",
+ "41": "parenright",
+ "42": "asterisk",
+ "43": "plus",
+ "44": "comma",
+ "45": "hyphen",
+ "173": "hyphen",
+ "46": "period",
+ "47": "slash",
+ "48": "zero",
+ "49": "one",
+ "50": "two",
+ "51": "three",
+ "52": "four",
+ "53": "five",
+ "54": "six",
+ "55": "seven",
+ "56": "eight",
+ "57": "nine",
+ "58": "colon",
+ "59": "semicolon",
+ "60": "less",
+ "61": "equal",
+ "62": "greater",
+ "63": "question",
+ "64": "at",
+ "65": "A",
+ "66": "B",
+ "67": "C",
+ "68": "D",
+ "69": "E",
+ "70": "F",
+ "71": "G",
+ "72": "H",
+ "73": "I",
+ "74": "J",
+ "75": "K",
+ "76": "L",
+ "77": "M",
+ "78": "N",
+ "79": "O",
+ "80": "P",
+ "81": "Q",
+ "82": "R",
+ "83": "S",
+ "84": "T",
+ "85": "U",
+ "86": "V",
+ "87": "W",
+ "88": "X",
+ "89": "Y",
+ "90": "Z",
+ "91": "bracketleft",
+ "92": "backslash",
+ "93": "bracketright",
+ "94": "asciicircum",
+ "95": "underscore",
+ "145": "quoteleft",
+ "97": "a",
+ "98": "b",
+ "99": "c",
+ "100": "d",
+ "101": "e",
+ "102": "f",
+ "103": "g",
+ "104": "h",
+ "105": "i",
+ "106": "j",
+ "107": "k",
+ "108": "l",
+ "109": "m",
+ "110": "n",
+ "111": "o",
+ "112": "p",
+ "113": "q",
+ "114": "r",
+ "115": "s",
+ "116": "t",
+ "117": "u",
+ "118": "v",
+ "119": "w",
+ "120": "x",
+ "121": "y",
+ "122": "z",
+ "123": "braceleft",
+ "124": "bar",
+ "125": "braceright",
+ "126": "asciitilde",
+ "161": "exclamdown",
+ "162": "cent",
+ "163": "sterling",
+ "165": "yen",
+ "131": "florin",
+ "167": "section",
+ "164": "currency",
+ "39": "quotesingle",
+ "147": "quotedblleft",
+ "171": "guillemotleft",
+ "139": "guilsinglleft",
+ "155": "guilsinglright",
+ "150": "endash",
+ "134": "dagger",
+ "135": "daggerdbl",
+ "183": "periodcentered",
+ "182": "paragraph",
+ "149": "bullet",
+ "130": "quotesinglbase",
+ "132": "quotedblbase",
+ "148": "quotedblright",
+ "187": "guillemotright",
+ "133": "ellipsis",
+ "137": "perthousand",
+ "191": "questiondown",
+ "96": "grave",
+ "180": "acute",
+ "136": "circumflex",
+ "152": "tilde",
+ "175": "macron",
+ "168": "dieresis",
+ "184": "cedilla",
+ "151": "emdash",
+ "198": "AE",
+ "170": "ordfeminine",
+ "216": "Oslash",
+ "140": "OE",
+ "186": "ordmasculine",
+ "230": "ae",
+ "248": "oslash",
+ "156": "oe",
+ "223": "germandbls",
+ "207": "Idieresis",
+ "233": "eacute",
+ "159": "Ydieresis",
+ "247": "divide",
+ "221": "Yacute",
+ "194": "Acircumflex",
+ "225": "aacute",
+ "219": "Ucircumflex",
+ "253": "yacute",
+ "234": "ecircumflex",
+ "220": "Udieresis",
+ "218": "Uacute",
+ "203": "Edieresis",
+ "169": "copyright",
+ "229": "aring",
+ "224": "agrave",
+ "227": "atilde",
+ "154": "scaron",
+ "237": "iacute",
+ "251": "ucircumflex",
+ "226": "acircumflex",
+ "231": "ccedilla",
+ "222": "Thorn",
+ "179": "threesuperior",
+ "210": "Ograve",
+ "192": "Agrave",
+ "215": "multiply",
+ "250": "uacute",
+ "255": "ydieresis",
+ "238": "icircumflex",
+ "202": "Ecircumflex",
+ "228": "adieresis",
+ "235": "edieresis",
+ "205": "Iacute",
+ "177": "plusminus",
+ "166": "brokenbar",
+ "174": "registered",
+ "200": "Egrave",
+ "142": "Zcaron",
+ "208": "Eth",
+ "199": "Ccedilla",
+ "193": "Aacute",
+ "196": "Adieresis",
+ "232": "egrave",
+ "211": "Oacute",
+ "243": "oacute",
+ "239": "idieresis",
+ "212": "Ocircumflex",
+ "217": "Ugrave",
+ "254": "thorn",
+ "178": "twosuperior",
+ "214": "Odieresis",
+ "181": "mu",
+ "236": "igrave",
+ "190": "threequarters",
+ "153": "trademark",
+ "204": "Igrave",
+ "189": "onehalf",
+ "244": "ocircumflex",
+ "241": "ntilde",
+ "201": "Eacute",
+ "188": "onequarter",
+ "138": "Scaron",
+ "176": "degree",
+ "242": "ograve",
+ "249": "ugrave",
+ "209": "Ntilde",
+ "245": "otilde",
+ "195": "Atilde",
+ "197": "Aring",
+ "213": "Otilde",
+ "206": "Icircumflex",
+ "172": "logicalnot",
+ "246": "odieresis",
+ "252": "udieresis",
+ "240": "eth",
+ "158": "zcaron",
+ "185": "onesuperior",
+ "128": "Euro"
+ },
+ "isUnicode": false,
+ "FontName": "Times-Roman",
+ "FullName": "Times Roman",
+ "FamilyName": "Times",
+ "Weight": "Roman",
+ "ItalicAngle": "0",
+ "IsFixedPitch": "false",
+ "CharacterSet": "ExtendedRoman",
+ "FontBBox": [
+ "-168",
+ "-218",
+ "1000",
+ "898"
+ ],
+ "UnderlinePosition": "-100",
+ "UnderlineThickness": "50",
+ "Version": "002.00",
+ "EncodingScheme": "WinAnsiEncoding",
+ "CapHeight": "662",
+ "XHeight": "450",
+ "Ascender": "683",
+ "Descender": "-217",
+ "StdHW": "28",
+ "StdVW": "84",
+ "StartCharMetrics": "317",
+ "C": {
+ "32": 250,
+ "160": 250,
+ "33": 333,
+ "34": 408,
+ "35": 500,
+ "36": 500,
+ "37": 833,
+ "38": 778,
+ "146": 333,
+ "40": 333,
+ "41": 333,
+ "42": 500,
+ "43": 564,
+ "44": 250,
+ "45": 333,
+ "173": 333,
+ "46": 250,
+ "47": 278,
+ "48": 500,
+ "49": 500,
+ "50": 500,
+ "51": 500,
+ "52": 500,
+ "53": 500,
+ "54": 500,
+ "55": 500,
+ "56": 500,
+ "57": 500,
+ "58": 278,
+ "59": 278,
+ "60": 564,
+ "61": 564,
+ "62": 564,
+ "63": 444,
+ "64": 921,
+ "65": 722,
+ "66": 667,
+ "67": 667,
+ "68": 722,
+ "69": 611,
+ "70": 556,
+ "71": 722,
+ "72": 722,
+ "73": 333,
+ "74": 389,
+ "75": 722,
+ "76": 611,
+ "77": 889,
+ "78": 722,
+ "79": 722,
+ "80": 556,
+ "81": 722,
+ "82": 667,
+ "83": 556,
+ "84": 611,
+ "85": 722,
+ "86": 722,
+ "87": 944,
+ "88": 722,
+ "89": 722,
+ "90": 611,
+ "91": 333,
+ "92": 278,
+ "93": 333,
+ "94": 469,
+ "95": 500,
+ "145": 333,
+ "97": 444,
+ "98": 500,
+ "99": 444,
+ "100": 500,
+ "101": 444,
+ "102": 333,
+ "103": 500,
+ "104": 500,
+ "105": 278,
+ "106": 278,
+ "107": 500,
+ "108": 278,
+ "109": 778,
+ "110": 500,
+ "111": 500,
+ "112": 500,
+ "113": 500,
+ "114": 333,
+ "115": 389,
+ "116": 278,
+ "117": 500,
+ "118": 500,
+ "119": 722,
+ "120": 500,
+ "121": 500,
+ "122": 444,
+ "123": 480,
+ "124": 200,
+ "125": 480,
+ "126": 541,
+ "161": 333,
+ "162": 500,
+ "163": 500,
+ "fraction": 167,
+ "165": 500,
+ "131": 500,
+ "167": 500,
+ "164": 500,
+ "39": 180,
+ "147": 444,
+ "171": 500,
+ "139": 333,
+ "155": 333,
+ "fi": 556,
+ "fl": 556,
+ "150": 500,
+ "134": 500,
+ "135": 500,
+ "183": 250,
+ "182": 453,
+ "149": 350,
+ "130": 333,
+ "132": 444,
+ "148": 444,
+ "187": 500,
+ "133": 1000,
+ "137": 1000,
+ "191": 444,
+ "96": 333,
+ "180": 333,
+ "136": 333,
+ "152": 333,
+ "175": 333,
+ "breve": 333,
+ "dotaccent": 333,
+ "168": 333,
+ "ring": 333,
+ "184": 333,
+ "hungarumlaut": 333,
+ "ogonek": 333,
+ "caron": 333,
+ "151": 1000,
+ "198": 889,
+ "170": 276,
+ "Lslash": 611,
+ "216": 722,
+ "140": 889,
+ "186": 310,
+ "230": 667,
+ "dotlessi": 278,
+ "lslash": 278,
+ "248": 500,
+ "156": 722,
+ "223": 500,
+ "207": 333,
+ "233": 444,
+ "abreve": 444,
+ "uhungarumlaut": 500,
+ "ecaron": 444,
+ "159": 722,
+ "247": 564,
+ "221": 722,
+ "194": 722,
+ "225": 444,
+ "219": 722,
+ "253": 500,
+ "scommaaccent": 389,
+ "234": 444,
+ "Uring": 722,
+ "220": 722,
+ "aogonek": 444,
+ "218": 722,
+ "uogonek": 500,
+ "203": 611,
+ "Dcroat": 722,
+ "commaaccent": 250,
+ "169": 760,
+ "Emacron": 611,
+ "ccaron": 444,
+ "229": 444,
+ "Ncommaaccent": 722,
+ "lacute": 278,
+ "224": 444,
+ "Tcommaaccent": 611,
+ "Cacute": 667,
+ "227": 444,
+ "Edotaccent": 611,
+ "154": 389,
+ "scedilla": 389,
+ "237": 278,
+ "lozenge": 471,
+ "Rcaron": 667,
+ "Gcommaaccent": 722,
+ "251": 500,
+ "226": 444,
+ "Amacron": 722,
+ "rcaron": 333,
+ "231": 444,
+ "Zdotaccent": 611,
+ "222": 556,
+ "Omacron": 722,
+ "Racute": 667,
+ "Sacute": 556,
+ "dcaron": 588,
+ "Umacron": 722,
+ "uring": 500,
+ "179": 300,
+ "210": 722,
+ "192": 722,
+ "Abreve": 722,
+ "215": 564,
+ "250": 500,
+ "Tcaron": 611,
+ "partialdiff": 476,
+ "255": 500,
+ "Nacute": 722,
+ "238": 278,
+ "202": 611,
+ "228": 444,
+ "235": 444,
+ "cacute": 444,
+ "nacute": 500,
+ "umacron": 500,
+ "Ncaron": 722,
+ "205": 333,
+ "177": 564,
+ "166": 200,
+ "174": 760,
+ "Gbreve": 722,
+ "Idotaccent": 333,
+ "summation": 600,
+ "200": 611,
+ "racute": 333,
+ "omacron": 500,
+ "Zacute": 611,
+ "142": 611,
+ "greaterequal": 549,
+ "208": 722,
+ "199": 667,
+ "lcommaaccent": 278,
+ "tcaron": 326,
+ "eogonek": 444,
+ "Uogonek": 722,
+ "193": 722,
+ "196": 722,
+ "232": 444,
+ "zacute": 444,
+ "iogonek": 278,
+ "211": 722,
+ "243": 500,
+ "amacron": 444,
+ "sacute": 389,
+ "239": 278,
+ "212": 722,
+ "217": 722,
+ "Delta": 612,
+ "254": 500,
+ "178": 300,
+ "214": 722,
+ "181": 500,
+ "236": 278,
+ "ohungarumlaut": 500,
+ "Eogonek": 611,
+ "dcroat": 500,
+ "190": 750,
+ "Scedilla": 556,
+ "lcaron": 344,
+ "Kcommaaccent": 722,
+ "Lacute": 611,
+ "153": 980,
+ "edotaccent": 444,
+ "204": 333,
+ "Imacron": 333,
+ "Lcaron": 611,
+ "189": 750,
+ "lessequal": 549,
+ "244": 500,
+ "241": 500,
+ "Uhungarumlaut": 722,
+ "201": 611,
+ "emacron": 444,
+ "gbreve": 500,
+ "188": 750,
+ "138": 556,
+ "Scommaaccent": 556,
+ "Ohungarumlaut": 722,
+ "176": 400,
+ "242": 500,
+ "Ccaron": 667,
+ "249": 500,
+ "radical": 453,
+ "Dcaron": 722,
+ "rcommaaccent": 333,
+ "209": 722,
+ "245": 500,
+ "Rcommaaccent": 667,
+ "Lcommaaccent": 611,
+ "195": 722,
+ "Aogonek": 722,
+ "197": 722,
+ "213": 722,
+ "zdotaccent": 444,
+ "Ecaron": 611,
+ "Iogonek": 333,
+ "kcommaaccent": 500,
+ "minus": 564,
+ "206": 333,
+ "ncaron": 500,
+ "tcommaaccent": 278,
+ "172": 564,
+ "246": 500,
+ "252": 500,
+ "notequal": 549,
+ "gcommaaccent": 500,
+ "240": 500,
+ "158": 444,
+ "ncommaaccent": 500,
+ "185": 300,
+ "imacron": 278,
+ "128": 500
+ },
+ "CIDtoGID_Compressed": true,
+ "CIDtoGID": "eJwDAAAAAAE=",
+ "_version_": 6
+}
\ No newline at end of file
|