1/* Copyright 2016 Software Freedom Conservancy Inc. 2 * 3 * This software is licensed under the GNU LGPL (version 2.1 or later). 4 * See the COPYING file in this distribution. 5 */ 6 7public enum ContentLayout { 8 STANDARD_SIZE, 9 CUSTOM_SIZE, 10 IMAGE_PER_PAGE 11} 12 13public class PrintSettings { 14 public const int MIN_CONTENT_PPI = 72; /* 72 ppi is the pixel resolution of a 14" VGA 15 display -- it's standard for historical reasons */ 16 public const int MAX_CONTENT_PPI = 1200; /* 1200 ppi is appropriate for a 3600 dpi imagesetter 17 used to produce photographic plates for commercial 18 printing -- it's the highest pixel resolution 19 commonly used */ 20 private ContentLayout content_layout; 21 private Measurement content_width; 22 private Measurement content_height; 23 private int content_ppi; 24 private int image_per_page_selection; 25 private int size_selection; 26 private bool match_aspect_ratio; 27 private bool print_titles; 28 private string print_titles_font; 29 30 public PrintSettings() { 31 Config.Facade config = Config.Facade.get_instance(); 32 33 MeasurementUnit units = (MeasurementUnit) config.get_printing_content_units(); 34 35 content_width = Measurement(config.get_printing_content_width(), units); 36 content_height = Measurement(config.get_printing_content_height(), units); 37 size_selection = config.get_printing_size_selection(); 38 content_layout = (ContentLayout) config.get_printing_content_layout(); 39 match_aspect_ratio = config.get_printing_match_aspect_ratio(); 40 print_titles = config.get_printing_print_titles(); 41 print_titles_font = config.get_printing_titles_font(); 42 image_per_page_selection = config.get_printing_images_per_page(); 43 content_ppi = config.get_printing_content_ppi(); 44 } 45 46 public void save() { 47 Config.Facade config = Config.Facade.get_instance(); 48 49 config.set_printing_content_units(content_width.unit); 50 config.set_printing_content_width(content_width.value); 51 config.set_printing_content_height(content_height.value); 52 config.set_printing_size_selection(size_selection); 53 config.set_printing_content_layout(content_layout); 54 config.set_printing_match_aspect_ratio(match_aspect_ratio); 55 config.set_printing_print_titles(print_titles); 56 config.set_printing_titles_font(print_titles_font); 57 config.set_printing_images_per_page(image_per_page_selection); 58 config.set_printing_content_ppi(content_ppi); 59 } 60 61 62 public Measurement get_content_width() { 63 switch (get_content_layout()) { 64 case ContentLayout.STANDARD_SIZE: 65 case ContentLayout.IMAGE_PER_PAGE: 66 return (PrintManager.get_instance().get_standard_sizes()[ 67 get_size_selection()]).width; 68 69 case ContentLayout.CUSTOM_SIZE: 70 return content_width; 71 72 default: 73 error("unknown ContentLayout enumeration value"); 74 } 75 } 76 77 public Measurement get_content_height() { 78 switch (get_content_layout()) { 79 case ContentLayout.STANDARD_SIZE: 80 case ContentLayout.IMAGE_PER_PAGE: 81 return (PrintManager.get_instance().get_standard_sizes()[ 82 get_size_selection()]).height; 83 84 case ContentLayout.CUSTOM_SIZE: 85 return content_height; 86 87 default: 88 error("unknown ContentLayout enumeration value"); 89 } 90 } 91 92 public Measurement get_minimum_content_dimension() { 93 return Measurement(0.5, MeasurementUnit.INCHES); 94 } 95 96 public Measurement get_maximum_content_dimension() { 97 return Measurement(30, MeasurementUnit.INCHES); 98 } 99 100 public bool is_match_aspect_ratio_enabled() { 101 return match_aspect_ratio; 102 } 103 104 public bool is_print_titles_enabled() { 105 return print_titles; 106 } 107 108 public int get_content_ppi() { 109 return content_ppi; 110 } 111 112 public int get_image_per_page_selection() { 113 return image_per_page_selection; 114 } 115 116 public int get_size_selection() { 117 return size_selection; 118 } 119 120 public ContentLayout get_content_layout() { 121 return content_layout; 122 } 123 124 public void set_content_layout(ContentLayout content_layout) { 125 this.content_layout = content_layout; 126 } 127 128 public void set_content_width(Measurement content_width) { 129 this.content_width = content_width; 130 } 131 132 public void set_content_height(Measurement content_height) { 133 this.content_height = content_height; 134 } 135 136 public void set_content_ppi(int content_ppi) { 137 this.content_ppi = content_ppi; 138 } 139 140 public void set_image_per_page_selection(int image_per_page_selection) { 141 this.image_per_page_selection = image_per_page_selection; 142 } 143 144 public void set_size_selection(int size_selection) { 145 this.size_selection = size_selection; 146 } 147 148 public void set_match_aspect_ratio_enabled(bool enable_state) { 149 this.match_aspect_ratio = enable_state; 150 } 151 152 public void set_print_titles_enabled(bool print_titles) { 153 this.print_titles = print_titles; 154 } 155 156 public void set_print_titles_font(string fontname) { 157 this.print_titles_font = fontname; 158 } 159 160 public string get_print_titles_font() { 161 return this.print_titles_font; 162 } 163} 164 165/* we define our own measurement enum instead of using the Gtk.Unit enum 166 provided by Gtk+ 2.0 because Gtk.Unit doesn't define a CENTIMETERS 167 constant (though it does define an MM for millimeters). This is 168 unfortunate, because in metric countries people like to think about 169 paper sizes for printing in CM not MM. so, to avoid having to 170 multiply and divide everything by 10 (which is error prone) to convert 171 from CM to MM and vice-versa whenever we read or write measurements, we 172 eschew Gtk.Unit and substitute our own */ 173public enum MeasurementUnit { 174 INCHES, 175 CENTIMETERS 176} 177 178public struct Measurement { 179 private const double CENTIMETERS_PER_INCH = 2.54; 180 private const double INCHES_PER_CENTIMETER = (1.0 / 2.54); 181 182 public double value; 183 public MeasurementUnit unit; 184 185 public Measurement(double value, MeasurementUnit unit) { 186 this.value = value; 187 this.unit = unit; 188 } 189 190 public Measurement convert_to(MeasurementUnit to_unit) { 191 if (unit == to_unit) 192 return this; 193 194 if (to_unit == MeasurementUnit.INCHES) { 195 return Measurement(value * INCHES_PER_CENTIMETER, MeasurementUnit.INCHES); 196 } else if (to_unit == MeasurementUnit.CENTIMETERS) { 197 return Measurement(value * CENTIMETERS_PER_INCH, MeasurementUnit.CENTIMETERS); 198 } else { 199 error("unrecognized unit"); 200 } 201 } 202 203 public bool is_less_than(Measurement rhs) { 204 Measurement converted_rhs = (unit == rhs.unit) ? rhs : rhs.convert_to(unit); 205 return (value < converted_rhs.value); 206 } 207 208 public bool is_greater_than(Measurement rhs) { 209 Measurement converted_rhs = (unit == rhs.unit) ? rhs : rhs.convert_to(unit); 210 return (value > converted_rhs.value); 211 } 212} 213 214private enum PrintLayout { 215 ENTIRE_PAGE, 216 TWO_PER_PAGE, 217 FOUR_PER_PAGE, 218 SIX_PER_PAGE, 219 EIGHT_PER_PAGE, 220 SIXTEEN_PER_PAGE, 221 THIRTY_TWO_PER_PAGE; 222 223 public static PrintLayout[] get_all() { 224 return { 225 ENTIRE_PAGE, 226 TWO_PER_PAGE, 227 FOUR_PER_PAGE, 228 SIX_PER_PAGE, 229 EIGHT_PER_PAGE, 230 SIXTEEN_PER_PAGE, 231 THIRTY_TWO_PER_PAGE 232 }; 233 } 234 235 public int get_per_page() { 236 int[] per_page = { 1, 2, 4, 6, 8, 16, 32 }; 237 238 return per_page[this]; 239 } 240 241 public int get_x() { 242 int[] x = { 1, 1, 2, 2, 2, 4, 4 }; 243 244 return x[this]; 245 } 246 247 public int get_y() { 248 int[] y = { 1, 2, 2, 3, 4, 4, 8 }; 249 250 return y[this]; 251 } 252 253 public string to_string() { 254 string[] labels = { 255 _("Fill the entire page"), 256 _("2 images per page"), 257 _("4 images per page"), 258 _("6 images per page"), 259 _("8 images per page"), 260 _("16 images per page"), 261 _("32 images per page") 262 }; 263 264 return labels[this]; 265 } 266} 267 268[GtkTemplate (ui = "/org/gnome/Shotwell/ui/printing_widget.ui")] 269public class CustomPrintTab : Gtk.Box { 270 private const int INCHES_COMBO_CHOICE = 0; 271 private const int CENTIMETERS_COMBO_CHOICE = 1; 272 273 [GtkChild] 274 private Gtk.RadioButton standard_size_radio; 275 [GtkChild] 276 private Gtk.RadioButton custom_size_radio; 277 [GtkChild] 278 private Gtk.RadioButton image_per_page_radio; 279 [GtkChild] 280 private Gtk.ComboBoxText image_per_page_combo; 281 [GtkChild] 282 private Gtk.ComboBoxText standard_sizes_combo; 283 [GtkChild] 284 private Gtk.ComboBoxText units_combo; 285 [GtkChild] 286 private Gtk.Entry custom_width_entry; 287 [GtkChild] 288 private Gtk.Entry custom_height_entry; 289 [GtkChild] 290 private Gtk.Entry ppi_entry; 291 [GtkChild] 292 private Gtk.CheckButton aspect_ratio_check; 293 [GtkChild] 294 private Gtk.CheckButton title_print_check; 295 [GtkChild] 296 private Gtk.FontButton title_print_font; 297 298 private Measurement local_content_width = Measurement(5.0, MeasurementUnit.INCHES); 299 private Measurement local_content_height = Measurement(5.0, MeasurementUnit.INCHES); 300 private int local_content_ppi; 301 private bool is_text_insertion_in_progress = false; 302 private PrintJob source_job; 303 304 public CustomPrintTab(PrintJob source_job) { 305 this.source_job = source_job; 306 307 standard_size_radio.clicked.connect(on_radio_group_click); 308 custom_size_radio.clicked.connect(on_radio_group_click); 309 image_per_page_radio.clicked.connect(on_radio_group_click); 310 311 foreach (PrintLayout layout in PrintLayout.get_all()) { 312 image_per_page_combo.append_text(layout.to_string()); 313 } 314 315 unowned StandardPrintSize[] standard_sizes = PrintManager.get_instance().get_standard_sizes(); 316 standard_sizes_combo.set_row_separator_func(standard_sizes_combo_separator_func); 317 foreach (StandardPrintSize size in standard_sizes) { 318 standard_sizes_combo.append_text(size.name); 319 } 320 321 standard_sizes_combo.set_active(9 * Resources.get_default_measurement_unit()); 322 323 custom_width_entry.insert_text.connect(on_entry_insert_text); 324 custom_width_entry.focus_out_event.connect(on_width_entry_focus_out); 325 326 custom_height_entry.insert_text.connect(on_entry_insert_text); 327 custom_height_entry.focus_out_event.connect(on_height_entry_focus_out); 328 329 units_combo.changed.connect(on_units_combo_changed); 330 units_combo.set_active(Resources.get_default_measurement_unit()); 331 332 ppi_entry.insert_text.connect(on_ppi_entry_insert_text); 333 ppi_entry.focus_out_event.connect(on_ppi_entry_focus_out); 334 335 sync_state_from_job(source_job); 336 337 show_all(); 338 339 /* connect this signal after state is sync'd */ 340 aspect_ratio_check.clicked.connect(on_aspect_ratio_check_clicked); 341 } 342 343 private void on_aspect_ratio_check_clicked() { 344 if (aspect_ratio_check.get_active()) { 345 local_content_width = 346 Measurement(local_content_height.value * source_job.get_source_aspect_ratio(), 347 local_content_height.unit); 348 custom_width_entry.set_text(format_measurement(local_content_width)); 349 } 350 } 351 352 private bool on_width_entry_focus_out(Gdk.EventFocus event) { 353 if (custom_width_entry.get_text() == (format_measurement_as(local_content_width, 354 get_user_unit_choice()))) 355 return false; 356 357 Measurement new_width = get_width_entry_value(); 358 Measurement min_width = source_job.get_local_settings().get_minimum_content_dimension(); 359 Measurement max_width = source_job.get_local_settings().get_maximum_content_dimension(); 360 361 if (new_width.is_less_than(min_width) || new_width.is_greater_than(max_width)) { 362 custom_width_entry.set_text(format_measurement(local_content_width)); 363 return false; 364 } 365 366 if (is_match_aspect_ratio_enabled()) { 367 Measurement new_height = 368 Measurement(new_width.value / source_job.get_source_aspect_ratio(), 369 new_width.unit); 370 local_content_height = new_height; 371 custom_height_entry.set_text(format_measurement(new_height)); 372 } 373 374 local_content_width = new_width; 375 custom_width_entry.set_text(format_measurement(new_width)); 376 return false; 377 } 378 379 private string format_measurement(Measurement measurement) { 380 return "%.2f".printf(measurement.value); 381 } 382 383 private string format_measurement_as(Measurement measurement, MeasurementUnit to_unit) { 384 Measurement converted_measurement = (measurement.unit == to_unit) ? measurement : 385 measurement.convert_to(to_unit); 386 return format_measurement(converted_measurement); 387 } 388 389 private bool on_ppi_entry_focus_out(Gdk.EventFocus event) { 390 set_content_ppi(int.parse(ppi_entry.get_text())); 391 return false; 392 } 393 394 private void on_ppi_entry_insert_text(Gtk.Editable editable, string text, int length, 395 ref int position) { 396 Gtk.Entry sender = (Gtk.Entry) editable; 397 398 if (is_text_insertion_in_progress) 399 return; 400 401 is_text_insertion_in_progress = true; 402 403 if (length == -1) 404 length = (int) text.length; 405 406 string new_text = ""; 407 for (int ctr = 0; ctr < length; ctr++) { 408 if (text[ctr].isdigit()) 409 new_text += ((char) text[ctr]).to_string(); 410 } 411 412 if (new_text.length > 0) 413 sender.insert_text(new_text, (int) new_text.length, ref position); 414 415 Signal.stop_emission_by_name(sender, "insert-text"); 416 417 is_text_insertion_in_progress = false; 418 } 419 420 private bool on_height_entry_focus_out(Gdk.EventFocus event) { 421 if (custom_height_entry.get_text() == (format_measurement_as(local_content_height, 422 get_user_unit_choice()))) 423 return false; 424 425 Measurement new_height = get_height_entry_value(); 426 Measurement min_height = source_job.get_local_settings().get_minimum_content_dimension(); 427 Measurement max_height = source_job.get_local_settings().get_maximum_content_dimension(); 428 429 if (new_height.is_less_than(min_height) || new_height.is_greater_than(max_height)) { 430 custom_height_entry.set_text(format_measurement(local_content_height)); 431 return false; 432 } 433 434 if (is_match_aspect_ratio_enabled()) { 435 Measurement new_width = 436 Measurement(new_height.value * source_job.get_source_aspect_ratio(), 437 new_height.unit); 438 local_content_width = new_width; 439 custom_width_entry.set_text(format_measurement(new_width)); 440 } 441 442 local_content_height = new_height; 443 custom_height_entry.set_text(format_measurement(new_height)); 444 return false; 445 } 446 447 private MeasurementUnit get_user_unit_choice() { 448 if (units_combo.get_active() == INCHES_COMBO_CHOICE) { 449 return MeasurementUnit.INCHES; 450 } else if (units_combo.get_active() == CENTIMETERS_COMBO_CHOICE) { 451 return MeasurementUnit.CENTIMETERS; 452 } else { 453 error("unknown unit combo box choice"); 454 } 455 } 456 457 private void set_user_unit_choice(MeasurementUnit unit) { 458 if (unit == MeasurementUnit.INCHES) { 459 units_combo.set_active(INCHES_COMBO_CHOICE); 460 } else if (unit == MeasurementUnit.CENTIMETERS) { 461 units_combo.set_active(CENTIMETERS_COMBO_CHOICE); 462 } else { 463 error("unknown MeasurementUnit enumeration"); 464 } 465 } 466 467 private Measurement get_width_entry_value() { 468 return Measurement(double.parse(custom_width_entry.get_text()), get_user_unit_choice()); 469 } 470 471 private Measurement get_height_entry_value() { 472 return Measurement(double.parse(custom_height_entry.get_text()), get_user_unit_choice()); 473 } 474 475 private void on_entry_insert_text(Gtk.Editable editable, string text, int length, 476 ref int position) { 477 478 Gtk.Entry sender = (Gtk.Entry) editable; 479 480 if (is_text_insertion_in_progress) 481 return; 482 483 is_text_insertion_in_progress = true; 484 485 if (length == -1) 486 length = (int) text.length; 487 488 unowned string decimal_point = Posix.nl_langinfo (Posix.NLItem.RADIXCHAR); 489 490 bool contains_decimal_point = sender.get_text().contains(decimal_point); 491 492 string new_text = ""; 493 for (int ctr = 0; ctr < length; ctr++) { 494 if (text[ctr].isdigit()) { 495 new_text += ((char) text[ctr]).to_string(); 496 } else if ((!contains_decimal_point) && (text[ctr] == decimal_point[0])) { 497 new_text += ((char) text[ctr]).to_string(); 498 } 499 } 500 501 if (new_text.length > 0) 502 sender.insert_text(new_text, (int) new_text.length, ref position); 503 504 Signal.stop_emission_by_name(sender, "insert-text"); 505 506 is_text_insertion_in_progress = false; 507 } 508 509 private void sync_state_from_job(PrintJob job) { 510 assert(job.get_local_settings().get_content_width().unit == 511 job.get_local_settings().get_content_height().unit); 512 513 Measurement constrained_width = job.get_local_settings().get_content_width(); 514 if (job.get_local_settings().is_match_aspect_ratio_enabled()) 515 constrained_width = Measurement(job.get_local_settings().get_content_height().value * 516 job.get_source_aspect_ratio(), job.get_local_settings().get_content_height().unit); 517 set_content_width(constrained_width); 518 set_content_height(job.get_local_settings().get_content_height()); 519 set_content_layout(job.get_local_settings().get_content_layout()); 520 set_content_ppi(job.get_local_settings().get_content_ppi()); 521 set_image_per_page_selection(job.get_local_settings().get_image_per_page_selection()); 522 set_size_selection(job.get_local_settings().get_size_selection()); 523 set_match_aspect_ratio_enabled(job.get_local_settings().is_match_aspect_ratio_enabled()); 524 set_print_titles_enabled(job.get_local_settings().is_print_titles_enabled()); 525 set_print_titles_font(job.get_local_settings().get_print_titles_font()); 526 } 527 528 private void on_radio_group_click(Gtk.Button b) { 529 Gtk.RadioButton sender = (Gtk.RadioButton) b; 530 531 if (sender == standard_size_radio) { 532 set_content_layout_control_state(ContentLayout.STANDARD_SIZE); 533 standard_sizes_combo.grab_focus(); 534 } else if (sender == custom_size_radio) { 535 set_content_layout_control_state(ContentLayout.CUSTOM_SIZE); 536 custom_height_entry.grab_focus(); 537 } else if (sender == image_per_page_radio) { 538 set_content_layout_control_state(ContentLayout.IMAGE_PER_PAGE); 539 } 540 } 541 542 private void on_units_combo_changed() { 543 custom_height_entry.set_text(format_measurement_as(local_content_height, 544 get_user_unit_choice())); 545 custom_width_entry.set_text(format_measurement_as(local_content_width, 546 get_user_unit_choice())); 547 } 548 549 private void set_content_layout_control_state(ContentLayout layout) { 550 switch (layout) { 551 case ContentLayout.STANDARD_SIZE: 552 standard_sizes_combo.set_sensitive(true); 553 units_combo.set_sensitive(false); 554 custom_width_entry.set_sensitive(false); 555 custom_height_entry.set_sensitive(false); 556 aspect_ratio_check.set_sensitive(false); 557 image_per_page_combo.set_sensitive(false); 558 break; 559 560 case ContentLayout.CUSTOM_SIZE: 561 standard_sizes_combo.set_sensitive(false); 562 units_combo.set_sensitive(true); 563 custom_width_entry.set_sensitive(true); 564 custom_height_entry.set_sensitive(true); 565 aspect_ratio_check.set_sensitive(true); 566 image_per_page_combo.set_sensitive(false); 567 break; 568 569 case ContentLayout.IMAGE_PER_PAGE: 570 standard_sizes_combo.set_sensitive(false); 571 units_combo.set_sensitive(false); 572 custom_width_entry.set_sensitive(false); 573 custom_height_entry.set_sensitive(false); 574 aspect_ratio_check.set_sensitive(false); 575 image_per_page_combo.set_sensitive(true); 576 break; 577 578 default: 579 error("unknown ContentLayout enumeration value"); 580 } 581 } 582 583 private static bool standard_sizes_combo_separator_func(Gtk.TreeModel model, 584 Gtk.TreeIter iter) { 585 Value val; 586 model.get_value(iter, 0, out val); 587 588 return (val.get_string() == "-"); 589 } 590 591 private void set_content_layout(ContentLayout content_layout) { 592 set_content_layout_control_state(content_layout); 593 switch (content_layout) { 594 case ContentLayout.STANDARD_SIZE: 595 standard_size_radio.set_active(true); 596 break; 597 598 case ContentLayout.CUSTOM_SIZE: 599 custom_size_radio.set_active(true); 600 break; 601 602 case ContentLayout.IMAGE_PER_PAGE: 603 image_per_page_radio.set_active(true); 604 break; 605 606 default: 607 error("unknown ContentLayout enumeration value"); 608 } 609 } 610 611 private ContentLayout get_content_layout() { 612 if (standard_size_radio.get_active()) 613 return ContentLayout.STANDARD_SIZE; 614 if (custom_size_radio.get_active()) 615 return ContentLayout.CUSTOM_SIZE; 616 if (image_per_page_radio.get_active()) 617 return ContentLayout.IMAGE_PER_PAGE; 618 619 error("inconsistent content layout radio button group state"); 620 } 621 622 private void set_content_width(Measurement content_width) { 623 if (content_width.unit != local_content_height.unit) { 624 set_user_unit_choice(content_width.unit); 625 local_content_height = local_content_height.convert_to(content_width.unit); 626 custom_height_entry.set_text(format_measurement(local_content_height)); 627 } 628 local_content_width = content_width; 629 custom_width_entry.set_text(format_measurement(content_width)); 630 } 631 632 private Measurement get_content_width() { 633 return local_content_width; 634 } 635 636 private void set_content_height(Measurement content_height) { 637 if (content_height.unit != local_content_width.unit) { 638 set_user_unit_choice(content_height.unit); 639 local_content_width = local_content_width.convert_to(content_height.unit); 640 custom_width_entry.set_text(format_measurement(local_content_width)); 641 } 642 local_content_height = content_height; 643 custom_height_entry.set_text(format_measurement(content_height)); 644 } 645 646 private Measurement get_content_height() { 647 return local_content_height; 648 } 649 650 private void set_content_ppi(int content_ppi) { 651 local_content_ppi = content_ppi.clamp(PrintSettings.MIN_CONTENT_PPI, 652 PrintSettings.MAX_CONTENT_PPI); 653 654 ppi_entry.set_text("%d".printf(local_content_ppi)); 655 } 656 657 private int get_content_ppi() { 658 return local_content_ppi; 659 } 660 661 private void set_image_per_page_selection(int image_per_page) { 662 image_per_page_combo.set_active(image_per_page); 663 } 664 665 private int get_image_per_page_selection() { 666 return image_per_page_combo.get_active(); 667 } 668 669 private void set_size_selection(int size_selection) { 670 standard_sizes_combo.set_active(size_selection); 671 } 672 673 private int get_size_selection() { 674 return standard_sizes_combo.get_active(); 675 } 676 677 private void set_match_aspect_ratio_enabled(bool enable_state) { 678 aspect_ratio_check.set_active(enable_state); 679 } 680 681 private void set_print_titles_enabled(bool print_titles) { 682 title_print_check.set_active(print_titles); 683 } 684 685 private void set_print_titles_font(string fontname) { 686 title_print_font.set_font_name(fontname); 687 } 688 689 690 private bool is_match_aspect_ratio_enabled() { 691 return aspect_ratio_check.get_active(); 692 } 693 694 private bool is_print_titles_enabled() { 695 return title_print_check.get_active(); 696 } 697 698 private string get_print_titles_font() { 699 return title_print_font.get_font_name(); 700 } 701 702 public PrintJob get_source_job() { 703 return source_job; 704 } 705 706 public PrintSettings get_local_settings() { 707 PrintSettings result = new PrintSettings(); 708 709 result.set_content_width(get_content_width()); 710 result.set_content_height(get_content_height()); 711 result.set_content_layout(get_content_layout()); 712 result.set_content_ppi(get_content_ppi()); 713 result.set_image_per_page_selection(get_image_per_page_selection()); 714 result.set_size_selection(get_size_selection()); 715 result.set_match_aspect_ratio_enabled(is_match_aspect_ratio_enabled()); 716 result.set_print_titles_enabled(is_print_titles_enabled()); 717 result.set_print_titles_font(get_print_titles_font()); 718 719 return result; 720 } 721} 722 723public class PrintJob : Gtk.PrintOperation { 724 private PrintSettings settings; 725 private Gee.ArrayList<Photo> photos = new Gee.ArrayList<Photo>(); 726 727 public PrintJob(Gee.Collection<Photo> to_print) { 728 this.settings = PrintManager.get_instance().get_global_settings(); 729 photos.add_all(to_print); 730 731 set_embed_page_setup (true); 732 double photo_aspect_ratio = photos[0].get_dimensions().get_aspect_ratio(); 733 if (photo_aspect_ratio < 1.0) 734 photo_aspect_ratio = 1.0 / photo_aspect_ratio; 735 } 736 737 public Gee.List<Photo> get_photos() { 738 return photos; 739 } 740 741 public Photo get_source_photo() { 742 return photos[0]; 743 } 744 745 public double get_source_aspect_ratio() { 746 double aspect_ratio = photos[0].get_dimensions().get_aspect_ratio(); 747 return (aspect_ratio < 1.0) ? (1.0 / aspect_ratio) : aspect_ratio; 748 } 749 750 public PrintSettings get_local_settings() { 751 return settings; 752 } 753 754 public void set_local_settings(PrintSettings settings) { 755 this.settings = settings; 756 } 757} 758 759public class StandardPrintSize { 760 public StandardPrintSize(string name, Measurement width, Measurement height) { 761 this.name = name; 762 this.width = width; 763 this.height = height; 764 } 765 766 public string name; 767 public Measurement width; 768 public Measurement height; 769} 770 771public class PrintManager { 772 private const double IMAGE_DISTANCE = 0.24; 773 774 private static PrintManager instance = null; 775 776 private PrintSettings settings; 777 private Gtk.PageSetup user_page_setup; 778 private CustomPrintTab custom_tab; 779 private ProgressDialog? progress_dialog = null; 780 private Cancellable? cancellable = null; 781 private StandardPrintSize[] standard_sizes = null; 782 783 private PrintManager() { 784 user_page_setup = new Gtk.PageSetup(); 785 settings = new PrintSettings(); 786 } 787 788 public unowned StandardPrintSize[] get_standard_sizes() { 789 if (standard_sizes == null) { 790 standard_sizes = new StandardPrintSize[0]; 791 792 standard_sizes += new StandardPrintSize(_("Wallet (2 × 3 in.)"), 793 Measurement(3, MeasurementUnit.INCHES), 794 Measurement(2, MeasurementUnit.INCHES)); 795 standard_sizes += new StandardPrintSize(_("Notecard (3 × 5 in.)"), 796 Measurement(5, MeasurementUnit.INCHES), 797 Measurement(3, MeasurementUnit.INCHES)); 798 standard_sizes += new StandardPrintSize(_("4 × 6 in."), 799 Measurement(6, MeasurementUnit.INCHES), 800 Measurement(4, MeasurementUnit.INCHES)); 801 standard_sizes += new StandardPrintSize(_("5 × 7 in."), 802 Measurement(7, MeasurementUnit.INCHES), 803 Measurement(5, MeasurementUnit.INCHES)); 804 standard_sizes += new StandardPrintSize(_("8 × 10 in."), 805 Measurement(10, MeasurementUnit.INCHES), 806 Measurement(8, MeasurementUnit.INCHES)); 807 standard_sizes += new StandardPrintSize(_("11 × 14 in."), 808 Measurement(14, MeasurementUnit.INCHES), 809 Measurement(11, MeasurementUnit.INCHES)); 810 standard_sizes += new StandardPrintSize(_("16 × 20 in."), 811 Measurement(20, MeasurementUnit.INCHES), 812 Measurement(16, MeasurementUnit.INCHES)); 813 standard_sizes += new StandardPrintSize(("-"), 814 Measurement(0, MeasurementUnit.INCHES), 815 Measurement(0, MeasurementUnit.INCHES)); 816 standard_sizes += new StandardPrintSize(_("Metric Wallet (9 × 13 cm)"), 817 Measurement(13, MeasurementUnit.CENTIMETERS), 818 Measurement(9, MeasurementUnit.CENTIMETERS)); 819 standard_sizes += new StandardPrintSize(_("Postcard (10 × 15 cm)"), 820 Measurement(15, MeasurementUnit.CENTIMETERS), 821 Measurement(10, MeasurementUnit.CENTIMETERS)); 822 standard_sizes += new StandardPrintSize(_("13 × 18 cm"), 823 Measurement(18, MeasurementUnit.CENTIMETERS), 824 Measurement(13, MeasurementUnit.CENTIMETERS)); 825 standard_sizes += new StandardPrintSize(_("18 × 24 cm"), 826 Measurement(24, MeasurementUnit.CENTIMETERS), 827 Measurement(18, MeasurementUnit.CENTIMETERS)); 828 standard_sizes += new StandardPrintSize(_("20 × 30 cm"), 829 Measurement(30, MeasurementUnit.CENTIMETERS), 830 Measurement(20, MeasurementUnit.CENTIMETERS)); 831 standard_sizes += new StandardPrintSize(_("24 × 40 cm"), 832 Measurement(40, MeasurementUnit.CENTIMETERS), 833 Measurement(24, MeasurementUnit.CENTIMETERS)); 834 standard_sizes += new StandardPrintSize(_("30 × 40 cm"), 835 Measurement(40, MeasurementUnit.CENTIMETERS), 836 Measurement(30, MeasurementUnit.CENTIMETERS)); 837 } 838 839 return standard_sizes; 840 } 841 842 public static PrintManager get_instance() { 843 if (instance == null) 844 instance = new PrintManager(); 845 846 return instance; 847 } 848 849 public void spool_photo(Gee.Collection<Photo> to_print) { 850 PrintJob job = new PrintJob(to_print); 851 job.set_custom_tab_label(_("Image Settings")); 852 job.set_unit(Gtk.Unit.INCH); 853 job.set_n_pages(1); 854 job.set_job_name(job.get_source_photo().get_name()); 855 job.set_default_page_setup(user_page_setup); 856 job.begin_print.connect(on_begin_print); 857 job.draw_page.connect(on_draw_page); 858 job.create_custom_widget.connect(on_create_custom_widget); 859 job.status_changed.connect(on_status_changed); 860 861 AppWindow.get_instance().set_busy_cursor(); 862 863 cancellable = new Cancellable(); 864 progress_dialog = new ProgressDialog(AppWindow.get_instance(), _("Printing…"), cancellable); 865 866 string? err_msg = null; 867 try { 868 Gtk.PrintOperationResult result = job.run(Gtk.PrintOperationAction.PRINT_DIALOG, 869 AppWindow.get_instance()); 870 if (result == Gtk.PrintOperationResult.APPLY) 871 user_page_setup = job.get_default_page_setup(); 872 } catch (Error e) { 873 job.cancel(); 874 err_msg = e.message; 875 } 876 877 progress_dialog.close(); 878 progress_dialog = null; 879 cancellable = null; 880 881 AppWindow.get_instance().set_normal_cursor(); 882 883 if (err_msg != null) 884 AppWindow.error_message(_("Unable to print photo:\n\n%s").printf(err_msg)); 885 } 886 887 private void on_begin_print(Gtk.PrintOperation emitting_object, Gtk.PrintContext job_context) { 888 debug("on_begin_print"); 889 890 PrintJob job = (PrintJob) emitting_object; 891 892 // cancel() can only be called from "begin-print", "paginate", or "draw-page" 893 if (cancellable != null && cancellable.is_cancelled()) { 894 job.cancel(); 895 896 return; 897 } 898 899 Gee.List<Photo> photos = job.get_photos(); 900 if (job.get_local_settings().get_content_layout() == ContentLayout.IMAGE_PER_PAGE){ 901 PrintLayout layout = (PrintLayout) job.get_local_settings().get_image_per_page_selection(); 902 job.set_n_pages((int) Math.ceil((double) photos.size / (double) layout.get_per_page())); 903 } else { 904 job.set_n_pages(photos.size); 905 } 906 907 spin_event_loop(); 908 } 909 910 private void on_status_changed(Gtk.PrintOperation job) { 911 debug("on_status_changed: %s", job.get_status_string()); 912 913 if (progress_dialog != null) { 914 progress_dialog.set_status(job.get_status_string()); 915 spin_event_loop(); 916 } 917 } 918 919 private void on_draw_page(Gtk.PrintOperation emitting_object, Gtk.PrintContext job_context, 920 int page_num) { 921 debug("on_draw_page"); 922 923 PrintJob job = (PrintJob) emitting_object; 924 925 // cancel() can only be called from "begin-print", "paginate", or "draw-page" 926 if (cancellable != null && cancellable.is_cancelled()) { 927 job.cancel(); 928 929 return; 930 } 931 932 spin_event_loop(); 933 934 Gtk.PageSetup page_setup = job_context.get_page_setup(); 935 double page_width = page_setup.get_page_width(Gtk.Unit.INCH); 936 double page_height = page_setup.get_page_height(Gtk.Unit.INCH); 937 938 double dpi = job.get_local_settings().get_content_ppi(); 939 double inv_dpi = 1.0 / dpi; 940 Cairo.Context dc = job_context.get_cairo_context(); 941 dc.scale(inv_dpi, inv_dpi); 942 Gee.List<Photo> photos = job.get_photos(); 943 944 ContentLayout content_layout = job.get_local_settings().get_content_layout(); 945 switch (content_layout) { 946 case ContentLayout.STANDARD_SIZE: 947 case ContentLayout.CUSTOM_SIZE: 948 double canvas_width, canvas_height; 949 if (content_layout == ContentLayout.STANDARD_SIZE) { 950 canvas_width = get_standard_sizes()[job.get_local_settings().get_size_selection()].width.convert_to( 951 MeasurementUnit.INCHES).value; 952 canvas_height = get_standard_sizes()[job.get_local_settings().get_size_selection()].height.convert_to( 953 MeasurementUnit.INCHES).value; 954 } else { 955 assert(content_layout == ContentLayout.CUSTOM_SIZE); 956 canvas_width = job.get_local_settings().get_content_width().convert_to( 957 MeasurementUnit.INCHES).value; 958 canvas_height = job.get_local_settings().get_content_height().convert_to( 959 MeasurementUnit.INCHES).value; 960 } 961 962 if (page_num < photos.size) { 963 Dimensions photo_dimensions = photos[page_num].get_dimensions(); 964 double photo_aspect_ratio = photo_dimensions.get_aspect_ratio(); 965 double canvas_aspect_ratio = ((double) canvas_width) / canvas_height; 966 if (Math.floor(canvas_aspect_ratio) != Math.floor(photo_aspect_ratio)) { 967 double canvas_tmp = canvas_width; 968 canvas_width = canvas_height; 969 canvas_height = canvas_tmp; 970 } 971 972 double dx = (page_width - canvas_width) / 2.0; 973 double dy = (page_height - canvas_height) / 2.0; 974 fit_image_to_canvas(photos[page_num], dx, dy, canvas_width, canvas_height, true, 975 job, job_context); 976 if (job.get_local_settings().is_print_titles_enabled()) { 977 add_title_to_canvas(page_width / 2, page_height, photos[page_num].get_name(), 978 job, job_context); 979 } 980 } 981 982 if (progress_dialog != null) 983 progress_dialog.monitor(page_num, photos.size); 984 break; 985 986 case ContentLayout.IMAGE_PER_PAGE: 987 PrintLayout layout = (PrintLayout) job.get_local_settings().get_image_per_page_selection(); 988 int nx = layout.get_x(); 989 int ny = layout.get_y(); 990 int start = page_num * layout.get_per_page(); 991 double canvas_width = (double) (page_width - IMAGE_DISTANCE * (nx - 1)) / nx; 992 double canvas_height = (double) (page_height - IMAGE_DISTANCE * (ny - 1)) / ny; 993 for (int y = 0; y < ny; y++){ 994 for (int x = 0; x < nx; x++){ 995 int i = start + y * nx + x; 996 if (i < photos.size) { 997 double dx = x * (canvas_width) + x * IMAGE_DISTANCE; 998 double dy = y * (canvas_height) + y * IMAGE_DISTANCE; 999 fit_image_to_canvas(photos[i], dx, dy, canvas_width, canvas_height, false, 1000 job, job_context); 1001 if (job.get_local_settings().is_print_titles_enabled()) { 1002 add_title_to_canvas(dx + canvas_width / 2, dy + canvas_height, 1003 photos[i].get_name(), job, job_context); 1004 } 1005 } 1006 1007 if (progress_dialog != null) 1008 progress_dialog.monitor(i, photos.size); 1009 } 1010 } 1011 break; 1012 1013 default: 1014 error("unknown or unsupported layout mode"); 1015 } 1016 } 1017 1018 private unowned Object on_create_custom_widget(Gtk.PrintOperation emitting_object) { 1019 custom_tab = new CustomPrintTab((PrintJob) emitting_object); 1020 ((PrintJob) emitting_object).custom_widget_apply.connect(on_custom_widget_apply); 1021 return custom_tab; 1022 } 1023 1024 private void on_custom_widget_apply(Gtk.Widget custom_widget) { 1025 CustomPrintTab tab = (CustomPrintTab) custom_widget; 1026 tab.get_source_job().set_local_settings(tab.get_local_settings()); 1027 set_global_settings(tab.get_local_settings()); 1028 } 1029 1030 private void fit_image_to_canvas(Photo photo, double x, double y, double canvas_width, double canvas_height, bool crop, PrintJob job, Gtk.PrintContext job_context) { 1031 Cairo.Context dc = job_context.get_cairo_context(); 1032 Dimensions photo_dimensions = photo.get_dimensions(); 1033 double photo_aspect_ratio = photo_dimensions.get_aspect_ratio(); 1034 double canvas_aspect_ratio = ((double) canvas_width) / canvas_height; 1035 1036 double target_width = 0.0; 1037 double target_height = 0.0; 1038 double dpi = job.get_local_settings().get_content_ppi(); 1039 1040 if (!crop) { 1041 if (canvas_aspect_ratio < photo_aspect_ratio) { 1042 target_width = canvas_width; 1043 target_height = target_width * (1.0 / photo_aspect_ratio); 1044 } else { 1045 target_height = canvas_height; 1046 target_width = target_height * photo_aspect_ratio; 1047 } 1048 x += (canvas_width - target_width) / 2.0; 1049 y += (canvas_height - target_height) / 2.0; 1050 } 1051 1052 double x_offset = dpi * x; 1053 double y_offset = dpi * y; 1054 dc.save(); 1055 dc.translate(x_offset, y_offset); 1056 1057 int w = (int) (dpi * canvas_width); 1058 int h = (int) (dpi * canvas_height); 1059 Dimensions viewport = Dimensions(w, h); 1060 1061 try { 1062 if (crop && !are_approximately_equal(canvas_aspect_ratio, photo_aspect_ratio)) { 1063 Scaling pixbuf_scaling = Scaling.to_fill_viewport(viewport); 1064 Gdk.Pixbuf photo_pixbuf = photo.get_pixbuf(pixbuf_scaling); 1065 Dimensions scaled_photo_dimensions = Dimensions.for_pixbuf(photo_pixbuf); 1066 int shave_vertical = 0; 1067 int shave_horizontal = 0; 1068 if (canvas_aspect_ratio < photo_aspect_ratio) { 1069 shave_vertical = (int) ((scaled_photo_dimensions.width - (scaled_photo_dimensions.height * canvas_aspect_ratio)) / 2.0); 1070 } else { 1071 shave_horizontal = (int) ((scaled_photo_dimensions.height - (scaled_photo_dimensions.width * (1.0 / canvas_aspect_ratio))) / 2.0); 1072 } 1073 Gdk.Pixbuf shaved_pixbuf = new Gdk.Pixbuf.subpixbuf(photo_pixbuf, shave_vertical,shave_horizontal, scaled_photo_dimensions.width - (2 * shave_vertical), scaled_photo_dimensions.height - (2 * shave_horizontal)); 1074 1075 photo_pixbuf = pixbuf_scaling.perform_on_pixbuf(shaved_pixbuf, Gdk.InterpType.HYPER, true); 1076 Gdk.cairo_set_source_pixbuf(dc, photo_pixbuf, 0.0, 0.0); 1077 } else { 1078 Scaling pixbuf_scaling = Scaling.for_viewport(viewport, true); 1079 Gdk.Pixbuf photo_pixbuf = photo.get_pixbuf(pixbuf_scaling); 1080 photo_pixbuf = pixbuf_scaling.perform_on_pixbuf(photo_pixbuf, Gdk.InterpType.HYPER, true); 1081 Gdk.cairo_set_source_pixbuf(dc, photo_pixbuf, 0.0, 0.0); 1082 } 1083 dc.paint(); 1084 1085 } catch (Error e) { 1086 job.cancel(); 1087 AppWindow.error_message(_("Unable to print photo:\n\n%s").printf(e.message)); 1088 } 1089 dc.restore(); 1090 } 1091 1092 private void add_title_to_canvas(double x, double y, string title, PrintJob job, Gtk.PrintContext job_context) { 1093 Cairo.Context dc = job_context.get_cairo_context(); 1094 double dpi = job.get_local_settings().get_content_ppi(); 1095 var title_font_description = Pango.FontDescription.from_string(job.get_local_settings().get_print_titles_font()); 1096 var title_layout = Pango.cairo_create_layout(dc); 1097 Pango.Context context = title_layout.get_context(); 1098 Pango.cairo_context_set_resolution (context, dpi); 1099 title_layout.set_font_description(title_font_description); 1100 title_layout.set_text(title, -1); 1101 int title_width, title_height; 1102 title_layout.get_pixel_size(out title_width, out title_height); 1103 double tx = dpi * x - title_width / 2; 1104 double ty = dpi * y - title_height; 1105 1106 // Transparent title text background 1107 dc.rectangle(tx - 10, ty + 2, title_width + 20, title_height); 1108 dc.set_source_rgba(1, 1, 1, 1); 1109 dc.set_line_width(2); 1110 dc.stroke_preserve(); 1111 dc.set_source_rgba(1, 1, 1, 0.5); 1112 dc.fill(); 1113 dc.set_source_rgba(0, 0, 0, 1); 1114 1115 dc.move_to(tx, ty + 2); 1116 Pango.cairo_show_layout(dc, title_layout); 1117 } 1118 1119 private bool are_approximately_equal(double val1, double val2) { 1120 double accept_err = 0.005; 1121 return (Math.fabs(val1 - val2) <= accept_err); 1122 } 1123 1124 public PrintSettings get_global_settings() { 1125 return settings; 1126 } 1127 1128 public void set_global_settings(PrintSettings settings) { 1129 this.settings = settings; 1130 settings.save(); 1131 } 1132} 1133