<commit_before># Copyright 2014 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Runs fio benchmarks.

Man: http://manpages.ubuntu.com/manpages/natty/man1/fio.1.html
Quick howto: http://www.bluestop.org/fio/HOWTO.txt
"""

import json
import logging
import posixpath
import re

import jinja2

from perfkitbenchmarker import data
from perfkitbenchmarker import flags
from perfkitbenchmarker import vm_util
from perfkitbenchmarker.packages import fio

LOCAL_JOB_FILE_NAME = 'fio.job'  # used with vm_util.PrependTempDir()
REMOTE_JOB_FILE_PATH = posixpath.join(vm_util.VM_TMP_DIR, 'fio.job')
DEFAULT_TEMP_FILE_NAME = 'fio-temp-file'
DISK_USABLE_SPACE_FRACTION = 0.9

# This dictionary maps scenario names to dictionaries of fio settings.
SCENARIOS = {
    'sequential_write': {
        'name': 'sequential_write',
        'rwkind': 'write',
        'blocksize': '512k'
    },
    'sequential_read': {
        'name': 'sequential_read',
        'rwkind': 'read',
        'blocksize': '512k'
    },
    'random_write': {
        'name': 'random_write',
        'rwkind': 'randwrite',
        'blocksize': '4k'
    },
    'random_read': {
        'name': 'random_read',
        'rwkind': 'randread',
        'blocksize': '4k'
    }
}

FLAGS = flags.FLAGS

flags.DEFINE_string('fio_jobfile', None,
                    'Job file that fio will use. If not given, use a job file '
                    'bundled with PKB. Cannot use with --generate_scenarios.')
flags.DEFINE_list('generate_scenarios', None,
                  'Generate a job file with the given scenarios. Special '
                  'scenario \'all\' generates all scenarios. Available '
                  'scenarios are sequential_write, sequential_read, '
                  'random_write, and random_read. Cannot use with '
                  '--fio_jobfile.')
flags.DEFINE_boolean('against_device', False,
                     'Unmount the device\'s filesystem so we can test against '
                     'the raw block device. If --generate-scenarios is given, '
                     'will generate a job file that uses the block device.')
flags.DEFINE_string('device_fill_size', '100%',
                    'The amount of device to fill in prepare stage. '
                    'The valid value can either be an integer, which '
                    'represents the number of bytes to fill or a '
                    'percentage, which represents the percentage '
                    'of the device.')
flags.DEFINE_string('io_depths', '1',
                    'IO queue depths to run on. Can specify a single number, '
                    'like --io_depths=1, or a range, like --io_depths=1-4')
flags.DEFINE_integer('working_set_size', None,
                     'The size of the working set, in GB. If not given, use '
                     'the full size of the device.',
                     lower_bound=0)

FLAGS_IGNORED_FOR_CUSTOM_JOBFILE = {
    'generate_scenarios', 'io_depths'}

IODEPTHS_REGEXP = re.compile(r'(\d+)(-(\d+))?')

def IODepthsValidator(string):
  match = IODEPTHS_REGEXP.match(string)
  return match and match.end() == len(string) and int(match.group(1)) > 0

flags.RegisterValidator('io_depths',
                        IODepthsValidator,
                        message='--io_depths must be an integer '
                                'or a range of integers, all > 0')

def GenerateFillCommand(fio_path, fill_path, fill_size):
  """Generate a command to sequentially write a device or file.

  Args:
    fio_path: path to the fio executable.
    fill_path: path to the device or file to fill.
    fill_size: amount of device or file to fill, in fio format.

  Returns:
    A string containing the command.
  """

  return (('sudo %s --filename=%s --ioengine=libaio '
           '--name=fill-device --blocksize=512k --iodepth=64 '
           '--rw=write --direct=1 --size=%s') %
          (fio_path, fill_path, fill_size))

BENCHMARK_INFO = {'name': 'fio',
                  'description': 'Runs fio in sequential, random, read '
                                 'and write modes.',
                  'scratch_disk': True,
                  'num_machines': 1}

GLOBALS_TEMPLATE = """
[global]
ioengine=libaio
invalidate=1
direct=1
runtime=10m
time_based
filename={{filename}}
do_verify=0
verify_fatal=0
randrepeat=0
"""

JOB_TEMPLATE = """
{% for iodepth in iodepths %}
[{{name}}-io-depth-{{iodepth}}]
stonewall
rw={{rwkind}}
blocksize={{blocksize}}
iodepth={{iodepth}}
size={{size}}
{% endfor %}
"""

def GetIODepths(io_depths):
  """Parse the io_depths parameter.

  Args:
    io_depths: a string in the format of the --io_depths flag.

  Returns:
    An iterable of integers.

  Raises:
    ValueError if io_depths doesn't follow a format it recognizes.
  """

  match = IODEPTHS_REGEXP.match(io_depths)

  if match.group(2) is None:
    return [int(match.group(1))]
  else:
    return range(int(match.group(1)), int(match.group(3)) + 1)

def GenerateJobFileString(disk, against_device,
                          scenarios, io_depths, working_set_size):
  """Write a fio job file.

  Args:
    disk: the disk.BaseDisk object we're benchmarking with.
    against_device: bool. True if we're using a raw disk.
    scenarios: iterable of dicts, taken from SCENARIOS.
    io_depths: iterable. The IO queue depths to test.
    working_set_size: int or None. If int, the size of the working set in GB.

  Returns:
    The contents of a fio job file, as a string.
  """

  if against_device:
    filename = disk.GetDevicePath()
  else:
    filename = posixpath.join(disk.mount_point, DEFAULT_TEMP_FILE_NAME)

  globals_template = jinja2.Template(GLOBALS_TEMPLATE,
                                     undefined=jinja2.StrictUndefined)
  job_template = jinja2.Template(JOB_TEMPLATE,
                                 undefined=jinja2.StrictUndefined)

  file_string = str(globals_template.render(filename=filename))
  size_string = str(working_set_size) + 'G' if working_set_size else '100%'
  for scenario in scenarios:
    file_string = file_string + str(job_template.render(
        name=scenario['name'],
        rwkind=scenario['rwkind'],
        blocksize=scenario['blocksize'],
        iodepths=io_depths,
        size=size_string))

  return file_string

def JobFileString(fio_jobfile, disk, against_device,
                  scenario_strings, io_depths, working_set_size):
  """Get the contents of our job file.

  Args:
    fio_jobfile: string or None. The path to the user's jobfile, if provided.
    disk: the disk.BaseDisk object we're benchmarking with.
    against_device: bool. True if we're using a raw disk.
    scenario_strings: list of strings or None. The workload scenarios to
      generate.
    io_depths: iterable. The IO queue depths to test.
    working_set_size: int or None. If int, the size of the working set in GB.
    vm: the virtual_machine.BaseVirtualMachine that we will run on.

  Returns:
    A string containing the user's job file.
  """

  # The default behavior is to run with the standard fio job file.
  if scenario_strings is None and not fio_jobfile:
    fio_jobfile = data.ResourcePath('fio.job')

  if fio_jobfile:
    with open(fio_jobfile, 'r') as jobfile:
      return jobfile.read()
  else:
    if 'all' in scenario_strings:
      scenarios = SCENARIOS.itervalues()
    else:
      for name in scenario_strings:
        if name not in SCENARIOS:
          logging.error('Unknown scenario name %s', name)
      scenarios = (SCENARIOS[name] for name in scenario_strings)

    return GenerateJobFileString(disk, against_device, scenarios,
                                 io_depths, working_set_size)

def GetInfo():
  return BENCHMARK_INFO

def Prepare(benchmark_spec):
  """Prepare the virtual machine to run FIO.

     This includes installing fio, bc, and libaio1 and pre-filling the
     attached disk. We also make sure the job file is always located
     at the same path on the local machine.

  Args:
    benchmark_spec: The benchmark specification. Contains all data that is
        required to run the benchmark.

  """

  if FLAGS.fio_jobfile:
    ignored_flags = {'--' + flag_name
                     for flag_name in FLAGS_IGNORED_FOR_CUSTOM_JOBFILE
                     if FLAGS[flag_name].present}

    if ignored_flags:
      logging.warning('Fio job file specified. Ignoring options "%s"',
                      ', '.join(ignored_flags))

  vm = benchmark_spec.vms[0]
  logging.info('FIO prepare on %s', vm)
  vm.Install('fio')

  # Fill the disk or file we're using
  disk = vm.scratch_disks[0]
  device_path = disk.GetDevicePath()
  mount_point = disk.mount_point
  if FLAGS.against_device:
    logging.info('Umount scratch disk on %s at %s', vm, mount_point)
    vm.RemoteCommand('sudo umount %s' % mount_point)

  if FLAGS.device_fill_size is not '0':
    if FLAGS.against_device:
      fill_path = device_path
      fill_size = FLAGS.device_fill_size
    else:
      fill_path = posixpath.join(mount_point, DEFAULT_TEMP_FILE_NAME)
      if FLAGS['disk_fill_size'].present:
        fill_size = FLAGS.disk_fill_size
      else:
        # Default to 90% of capacity because the file system will add
        # some overhead.
        fill_size = str(DISK_USABLE_SPACE_FRACTION *
                        1000 * disk.disk_size) + 'M'

    logging.info('Fill file %s on %s', fill_path, vm)
    command = GenerateFillCommand(fio.FIO_PATH,
                                  fill_path,
                                  fill_size)
    vm.RemoteCommand(command)

  job_file_path = vm_util.PrependTempDir(LOCAL_JOB_FILE_NAME)
  with open(job_file_path, 'w') as job_file:
    job_file.write(JobFileString(FLAGS.fio_jobfile,
                                 disk,
                                 FLAGS.against_device,
                                 FLAGS.generate_scenarios,
                                 GetIODepths(FLAGS.io_depths),
                                 FLAGS.working_set_size))
    logging.info('Wrote fio job file at %s', job_file_path)

  vm.PushFile(job_file_path, REMOTE_JOB_FILE_PATH)

def Run(benchmark_spec):
  """Spawn fio and gather the results.

  Args:
    benchmark_spec: The benchmark specification. Contains all data that is
        required to run the benchmark.

  Returns:
    A list of samples in the form of 3 or 4 tuples. The tuples contain
        the sample metric (string), value (float), and unit (string).
        If a 4th element is included, it is a dictionary of sample
        metadata.
  """
  vm = benchmark_spec.vms[0]
  logging.info('FIO running on %s', vm)

  fio_command = 'sudo %s --output-format=json %s' % (fio.FIO_PATH,
                                                     REMOTE_JOB_FILE_PATH)
  # TODO(user): This only gives results at the end of a job run
  #      so the program pauses here with no feedback to the user.
  #      This is a pretty lousy experience.
  logging.info('FIO Results:')
  stdout, stderr = vm.RemoteCommand(fio_command, should_log=True)

  disk = vm.scratch_disks[0]
  return fio.ParseResults(JobFileString(FLAGS.fio_jobfile,
                                        disk,
                                        FLAGS.against_device,
                                        FLAGS.generate_scenarios,
                                        GetIODepths(FLAGS.io_depths),
                                        FLAGS.working_set_size),
                          json.loads(stdout))

def Cleanup(benchmark_spec):
  """Uninstall packages required for fio and remove benchmark files.

  Args:
    benchmark_spec: The benchmark specification. Contains all data that is
        required to run the benchmark.
  """
  vm = benchmark_spec.vms[0]
  logging.info('FIO Cleanup up on %s', vm)
  vm.RemoveFile(REMOTE_JOB_FILE_PATH)
  if not FLAGS.against_device and not FLAGS.fio_jobfile:
    # If the user supplies their own job file, then they have to clean
    # up after themselves, because we don't know their temp file name.
    vm.RemoveFile(posixpath.join(vm.GetScratchDir(), DEFAULT_TEMP_FILE_NAME))
<commit_msg>Readability fixes in fio_benchmark.py<commit_after># Copyright 2014 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Runs fio benchmarks.

Man: http://manpages.ubuntu.com/manpages/natty/man1/fio.1.html
Quick howto: http://www.bluestop.org/fio/HOWTO.txt
"""

import json
import logging
import posixpath
import re

import jinja2

from perfkitbenchmarker import data
from perfkitbenchmarker import flags
from perfkitbenchmarker import vm_util
from perfkitbenchmarker.packages import fio

LOCAL_JOB_FILE_NAME = 'fio.job'  # used with vm_util.PrependTempDir()
REMOTE_JOB_FILE_PATH = posixpath.join(vm_util.VM_TMP_DIR, 'fio.job')
DEFAULT_TEMP_FILE_NAME = 'fio-temp-file'
DISK_USABLE_SPACE_FRACTION = 0.9

# This dictionary maps scenario names to dictionaries of fio settings.
SCENARIOS = {
    'sequential_write': {
        'name': 'sequential_write',
        'rwkind': 'write',
        'blocksize': '512k'
    },
    'sequential_read': {
        'name': 'sequential_read',
        'rwkind': 'read',
        'blocksize': '512k'
    },
    'random_write': {
        'name': 'random_write',
        'rwkind': 'randwrite',
        'blocksize': '4k'
    },
    'random_read': {
        'name': 'random_read',
        'rwkind': 'randread',
        'blocksize': '4k'
    }
}

FLAGS = flags.FLAGS

flags.DEFINE_string('fio_jobfile', None,
                    'Job file that fio will use. If not given, use a job file '
                    'bundled with PKB. Cannot use with --generate_scenarios.')
flags.DEFINE_list('generate_scenarios', None,
                  'Generate a job file with the given scenarios. Special '
                  'scenario \'all\' generates all scenarios. Available '
                  'scenarios are sequential_write, sequential_read, '
                  'random_write, and random_read. Cannot use with '
                  '--fio_jobfile.')
flags.DEFINE_boolean('against_device', False,
                     'Unmount the device\'s filesystem so we can test against '
                     'the raw block device. If --generate-scenarios is given, '
                     'will generate a job file that uses the block device.')
flags.DEFINE_string('device_fill_size', '100%',
                    'The amount of device to fill in prepare stage. '
                    'The valid value can either be an integer, which '
                    'represents the number of bytes to fill or a '
                    'percentage, which represents the percentage '
                    'of the device. Default to filling 100% of a raw device or '
                    '90% of a filesystem.')
flags.DEFINE_string('io_depths', '1',
                    'IO queue depths to run on. Can specify a single number, '
                    'like --io_depths=1, or a range, like --io_depths=1-4')
flags.DEFINE_integer('working_set_size', None,
                     'The size of the working set, in GB. If not given, use '
                     'the full size of the device.',
                     lower_bound=0)

FLAGS_IGNORED_FOR_CUSTOM_JOBFILE = {
    'generate_scenarios', 'io_depths'}

IODEPTHS_REGEXP = re.compile(r'(\d+)(-(\d+))?')

def IODepthsValidator(string):
  match = IODEPTHS_REGEXP.match(string)
  return match and match.end() == len(string) and int(match.group(1)) > 0

flags.RegisterValidator('io_depths',
                        IODepthsValidator,
                        message='--io_depths must be an integer '
                                'or a range of integers, all > 0')

def GenerateFillCommand(fio_path, fill_path, fill_size):
  """Generate a command to sequentially write a device or file.

  Args:
    fio_path: path to the fio executable.
    fill_path: path to the device or file to fill.
    fill_size: amount of device or file to fill, in fio format.

  Returns:
    A string containing the command.
  """

  return (('sudo %s --filename=%s --ioengine=libaio '
           '--name=fill-device --blocksize=512k --iodepth=64 '
           '--rw=write --direct=1 --size=%s') %
          (fio_path, fill_path, fill_size))

BENCHMARK_INFO = {'name': 'fio',
                  'description': 'Runs fio in sequential, random, read '
                                 'and write modes.',
                  'scratch_disk': True,
                  'num_machines': 1}

GLOBALS_TEMPLATE = """
[global]
ioengine=libaio
invalidate=1
direct=1
runtime=10m
time_based
filename={{filename}}
do_verify=0
verify_fatal=0
randrepeat=0
"""

JOB_TEMPLATE = """
{% for iodepth in iodepths %}
[{{name}}-io-depth-{{iodepth}}]
stonewall
rw={{rwkind}}
blocksize={{blocksize}}
iodepth={{iodepth}}
size={{size}}
{% endfor %}
"""

def GetIODepths(io_depths):
  """Parse the io_depths parameter.

  Args:
    io_depths: a string in the format of the --io_depths flag.

  Returns:
    An iterable of integers.

  Raises:
    ValueError if io_depths doesn't follow a format it recognizes.
  """

  match = IODEPTHS_REGEXP.match(io_depths)

  if match.group(2) is None:
    return [int(match.group(1))]
  else:
    return range(int(match.group(1)), int(match.group(3)) + 1)

def GenerateJobFileString(disk, against_device,
                          scenarios, io_depths, working_set_size):
  """Make a string with our fio job file.

  Args:
    disk: the disk.BaseDisk object we're benchmarking with.
    against_device: bool. True if we're using a raw disk.
    scenarios: iterable of dicts, taken from SCENARIOS.
    io_depths: iterable. The IO queue depths to test.
    working_set_size: int or None. If int, the size of the working set in GB.

  Returns:
    The contents of a fio job file, as a string.
  """

  if against_device:
    filename = disk.GetDevicePath()
  else:
    filename = posixpath.join(disk.mount_point, DEFAULT_TEMP_FILE_NAME)

  globals_template = jinja2.Template(GLOBALS_TEMPLATE,
                                     undefined=jinja2.StrictUndefined)
  job_template = jinja2.Template(JOB_TEMPLATE,
                                 undefined=jinja2.StrictUndefined)

  file_string = str(globals_template.render(filename=filename))
  size_string = str(working_set_size) + 'G' if working_set_size else '100%'
  for scenario in scenarios:
    file_string = file_string + str(job_template.render(
        name=scenario['name'],
        rwkind=scenario['rwkind'],
        blocksize=scenario['blocksize'],
        iodepths=io_depths,
        size=size_string))

  return file_string

def JobFileString(fio_jobfile, disk, against_device,
                  scenario_strings, io_depths, working_set_size):
  """Get the contents of the fio job file we're working with.

  This will either read the user's job file, if given, or generate a
  new one.

  Args:
    fio_jobfile: string or None. The path to the user's jobfile, if provided.
    disk: the disk.BaseDisk object we're benchmarking with.
    against_device: bool. True if we're using a raw disk.
    scenario_strings: list of strings or None. The workload scenarios to
      generate.
    io_depths: iterable. The IO queue depths to test.
    working_set_size: int or None. If int, the size of the working set in GB.

  Returns:
    A string containing a fio job file.
  """

  # The default behavior is to run with the standard fio job file.
  if scenario_strings is None and not fio_jobfile:
    fio_jobfile = data.ResourcePath('fio.job')

  if fio_jobfile:
    with open(fio_jobfile, 'r') as jobfile:
      return jobfile.read()
  else:
    if 'all' in scenario_strings:
      scenarios = SCENARIOS.itervalues()
    else:
      for name in scenario_strings:
        if name not in SCENARIOS:
          logging.error('Unknown scenario name %s', name)
      scenarios = (SCENARIOS[name] for name in scenario_strings)

    return GenerateJobFileString(disk, against_device, scenarios,
                                 io_depths, working_set_size)

def GetInfo():
  return BENCHMARK_INFO

def Prepare(benchmark_spec):
  """Prepare the virtual machine to run FIO.

     This includes installing fio, bc, and libaio1 and pre-filling the
     attached disk. We also make sure the job file is always located
     at the same path on the local machine.

  Args:
    benchmark_spec: The benchmark specification. Contains all data that is
        required to run the benchmark.

  """

  if FLAGS.fio_jobfile:
    ignored_flags = {'--' + flag_name
                     for flag_name in FLAGS_IGNORED_FOR_CUSTOM_JOBFILE
                     if FLAGS[flag_name].present}

    if ignored_flags:
      logging.warning('Fio job file specified. Ignoring options "%s"',
                      ', '.join(ignored_flags))

  vm = benchmark_spec.vms[0]
  logging.info('FIO prepare on %s', vm)
  vm.Install('fio')

  # Fill the disk or file we're using
  disk = vm.scratch_disks[0]
  device_path = disk.GetDevicePath()
  mount_point = disk.mount_point
  if FLAGS.against_device:
    logging.info('Umount scratch disk on %s at %s', vm, mount_point)
    vm.RemoteCommand('sudo umount %s' % mount_point)

  if FLAGS.device_fill_size is not '0':
    if FLAGS.against_device:
      fill_path = device_path
      fill_size = FLAGS.device_fill_size
    else:
      fill_path = posixpath.join(mount_point, DEFAULT_TEMP_FILE_NAME)
      if FLAGS['disk_fill_size'].present:
        fill_size = FLAGS.disk_fill_size
      else:
        # Default to 90% of capacity because the file system will add
        # some overhead.
        fill_size = str(DISK_USABLE_SPACE_FRACTION *
                        1000 * disk.disk_size) + 'M'

    logging.info('Fill file %s on %s', fill_path, vm)
    command = GenerateFillCommand(fio.FIO_PATH,
                                  fill_path,
                                  fill_size)
    vm.RemoteCommand(command)

  job_file_path = vm_util.PrependTempDir(LOCAL_JOB_FILE_NAME)
  with open(job_file_path, 'w') as job_file:
    job_file.write(JobFileString(FLAGS.fio_jobfile,
                                 disk,
                                 FLAGS.against_device,
                                 FLAGS.generate_scenarios,
                                 GetIODepths(FLAGS.io_depths),
                                 FLAGS.working_set_size))
    logging.info('Wrote fio job file at %s', job_file_path)

  vm.PushFile(job_file_path, REMOTE_JOB_FILE_PATH)

def Run(benchmark_spec):
  """Spawn fio and gather the results.

  Args:
    benchmark_spec: The benchmark specification. Contains all data that is
        required to run the benchmark.

  Returns:
    A list of sample.Sample objects.
  """
  vm = benchmark_spec.vms[0]
  logging.info('FIO running on %s', vm)

  fio_command = 'sudo %s --output-format=json %s' % (fio.FIO_PATH,
                                                     REMOTE_JOB_FILE_PATH)
  # TODO(user): This only gives results at the end of a job run
  #      so the program pauses here with no feedback to the user.
  #      This is a pretty lousy experience.
  logging.info('FIO Results:')
  stdout, stderr = vm.RemoteCommand(fio_command, should_log=True)

  disk = vm.scratch_disks[0]
  return fio.ParseResults(JobFileString(FLAGS.fio_jobfile,
                                        disk,
                                        FLAGS.against_device,
                                        FLAGS.generate_scenarios,
                                        GetIODepths(FLAGS.io_depths),
                                        FLAGS.working_set_size),
                          json.loads(stdout))

def Cleanup(benchmark_spec):
  """Uninstall packages required for fio and remove benchmark files.

  Args:
    benchmark_spec: The benchmark specification. Contains all data that is
        required to run the benchmark.
  """
  vm = benchmark_spec.vms[0]
  logging.info('FIO Cleanup up on %s', vm)
  vm.RemoveFile(REMOTE_JOB_FILE_PATH)
  if not FLAGS.against_device and not FLAGS.fio_jobfile:
    # If the user supplies their own job file, then they have to clean
    # up after themselves, because we don't know their temp file name.
    vm.RemoveFile(posixpath.join(vm.GetScratchDir(), DEFAULT_TEMP_FILE_NAME))
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
import swapper

class Migration(migrations.Migration):

    dependencies = [
        ('petition', '0006_auto_20150728_0005'),
    ]

    operations = [
        migrations.AlterModelOptions(
            name='petition',
            options={'swappable': swapper.swappable_setting('petition', 'Petition'),},
        ),
    ]
<commit_msg>Update 0007_auto_20150728_0239.py<commit_after># -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
import swapper

class Migration(migrations.Migration):

    dependencies = [
        ('petition', '0006_auto_20150728_0005'),
    ]

    operations = [
        migrations.AlterModelOptions(
            name='petition',
            options={'swappable': swapper.swappable_setting('petition', 'Petition')},
        ),
    ]
<|endoftext|>


<commit_before># coding: utf8

#  WeasyPrint converts web documents (HTML, CSS, ...) to PDF.
#  Copyright (C) 2011  Simon Sapin
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU Affero General Public License as
#  published by the Free Software Foundation, either version 3 of the
#  License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU Affero General Public License for more details.
#
#  You should have received a copy of the GNU Affero General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import division
import urllib
try:
    from urlparse import urljoin
except ImportError:
    # Python 3
    from urllib.parse import urljoin

import cairo
import cssutils.css.value

from ..css.utils import (get_single_keyword, get_keyword,
                         get_pixel_value, get_percentage_value)
from ..formatting_structure import boxes
from .. import text
from .figures import Point, Line, Trapezoid

def get_image_surface_from_uri(uri):
    try:
        fileimage = urllib.urlopen(uri)
        if fileimage.info().gettype() != 'image/png':
            raise NotImplementedError("Only png images are implemented")
        return cairo.ImageSurface.create_from_png(fileimage)
    except IOError:
        return None

def draw_box(context, box):
    if has_background(box):
        draw_background(context, box)

    if isinstance(box, boxes.TextBox):
        draw_text(context, box)
        return

    if isinstance(box, boxes.ReplacedBox):
        draw_replacedbox(context, box)

    if isinstance(box, boxes.ParentBox):
        for child in box.children:
            draw_box(context, child)

    draw_border(context, box)

def has_background(box):
    """
    Return the given box has any background.
    """
    return box.style['background-color'][0].alpha > 0 or \
        get_single_keyword(box.style['background-image']) != 'none'

def draw_canvas_background(context, page):
    """
    Draw the canvas’s background, taken from the root element.

    If the root element is "html" and has no background, the canvas’s
    background is taken from its "body" child.

    In both cases the background position is the same as if it was drawn on
    the element.

    See http://www.w3.org/TR/CSS21/colors.html#background
    """
    if has_background(page.root_box):
        draw_background(context, page.root_box, on_entire_canvas=True)
    elif page.root_box.element.tag.lower() == 'html':
        for child in page.root_box.children:
            if child.element.tag.lower() == 'body':
                # This must be drawn now, before anything on the root element.
                draw_background(context, child, on_entire_canvas=True)

def draw_background(context, box, on_entire_canvas=False):
    """
    Draw the box background color and image to a Cairo context.
    """
    if getattr(box, 'background_drawn', False):
        return

    box.background_drawn = True

    if not has_background(box):
        return

    with context.stacked():
        # Change coordinates to make the rest easier.
        context.translate(box.border_box_x(), box.border_box_y())
        bg_width = box.border_width()
        bg_height = box.border_height()

        if not on_entire_canvas:
            context.rectangle(0, 0, bg_width, bg_height)
            context.clip()

        # Background color
        bg_color = box.style['background-color'][0]
        if bg_color.alpha > 0:
            context.set_source_colorvalue(bg_color)
            context.paint()

        # Background image
        bg_image = box.style['background-image'][0]
        if bg_image.type != 'URI':
            return

        surface = get_image_surface_from_uri(bg_image.absoluteUri)
        if not surface:
            return

        image_width = surface.get_width()
        image_height = surface.get_height()

        context.translate(*absolute_background_position(
            box.style['background-position'], bg_width, bg_height,
            image_width, image_height))

        bg_repeat = get_single_keyword(box.style['background-repeat'])
        if bg_repeat != 'repeat':
            # Get the current clip rectangle
            clip_x1, clip_y1, clip_x2, clip_y2 = context.clip_extents()
            clip_width = clip_x2 - clip_x1
            clip_height = clip_y2 - clip_y1

            if bg_repeat in ('no-repeat', 'repeat-x'):
                # Limit the drawn area vertically
                clip_y1 = 0
                clip_height = image_height

            if bg_repeat in ('no-repeat', 'repeat-y'):
                # Limit the drawn area horizontally
                clip_x1 = 0
                clip_width = image_width

            # Second clip for the background image
            context.rectangle(clip_x1, clip_y1, clip_width, clip_height)
            context.clip()

        pattern = cairo.SurfacePattern(surface)
        pattern.set_extend(cairo.EXTEND_REPEAT)
        context.set_source(pattern)
        context.paint()

def make_css_value(value):
    return cssutils.css.value.PropertyValue(value)[0]

def absolute_background_position(css_values, bg_width, bg_height,
                                 image_width, image_height):
    """
    Parse cssutils Value objects for background-position and return
    (position_x, position_y) relative to the background’s top left corner,
    in pixels.

    http://www.w3.org/TR/CSS21/colors.html#propdef-background-position
    """
    if len(css_values) == 1:
        css_values = [css_values[0], make_css_value('center')]
    else:
        assert len(css_values) == 2

    horizontal = None
    vertical = None
    non_keywords = []
    for value in css_values:
        keyword = get_keyword(value)
        if keyword == 'top':
            assert vertical is None
            vertical = make_css_value('0%')
        elif keyword == 'bottom':
            assert vertical is None
            vertical = make_css_value('100%')
        elif keyword == 'left':
            assert horizontal is None
            horizontal = make_css_value('0%')
        elif keyword == 'right':
            assert horizontal is None
            horizontal = make_css_value('100%')
        elif keyword == 'center':
            if horizontal is None:
                horizontal = make_css_value('50%')
            else:
                vertical = make_css_value('50%')
        else:
            assert keyword is None
            non_keywords.append(value)

    # In that order. If both value are non-keyword, the first one
    # is horizontal and the second one vertical. list.pop() gives
    # the last value.
    if vertical is None:
        vertical = non_keywords.pop()
    if horizontal is None:
        horizontal = non_keywords.pop()

    vertical_pixels = background_position_value(
        vertical, bg_height, image_height)
    horizontal_pixels = background_position_value(
        horizontal, bg_width, image_width)
    return horizontal_pixels, vertical_pixels

def background_position_value(value, bg_size, image_size):
    pixels = get_pixel_value(value)
    if pixels is not None:
        return pixels

    percentage = get_percentage_value(value)
    assert percentage is not None
    return (bg_size - image_size) * percentage / 100.

def draw_border(context, box):
    """
    Draw the box border to a Cairo context.
    """
    # TODO: implement border-spacing, border-collapse and the other border style

    def get_edge(x, y, width, height):
        return (Point(x,y), Point(x+width,y), Point(x+width, y+height),
                Point(x, y+height))

    def get_border_area():
        # border area
        x = box.position_x + box.margin_left
        y = box.position_y + box.margin_top
        border_edge = get_edge(x, y, box.border_width(), box.border_height())
        # padding area
        x = x + box.border_left_width
        y = y + box.border_top_width
        padding_edge = get_edge(x, y, box.padding_width(), box.padding_height())
        return border_edge, padding_edge

    def get_lines(rectangle):
        n = len (rectangle)
        for i, point in enumerate(rectangle):
            yield Line(rectangle[i], rectangle[(i+1) % n])

    def get_trapezoids():
        border_edge, padding_edge = get_border_area()
        for line1,line2 in zip(get_lines(border_edge), get_lines(padding_edge)):
            yield Trapezoid(line1, line2)

    def draw_border_side(side, trapezoid):
        width = float(box.style['border-%s-width'%side][0].value)
        if box.style['border-%s-width'%side] == 0:
            return
        color = box.style['border-%s-color'%side][0]
        style = box.style['border-%s-style'%side][0].value
        if color.alpha > 0:
            with context.stacked():
                if not style in ["dotted", "dashed"]:
                    trapezoid.draw_path(context)
                    context.clip()
                elif style == "dotted":
                    #TODO:Find a way to make a real dotted border
                    context.set_dash([width], 0)
                elif style == "dashed":
                    #TODO:Find a way to make a real dashed border
                    context.set_dash([4*width], 0)
                line = trapezoid.get_middle_line()
                line.draw_path(context)
                context.set_source_colorvalue(color)
                context.set_line_width(width)
                context.stroke()

    trapezoids_side = zip(["top", "right", "bottom", "left"], get_trapezoids())

    for side, trapezoid in trapezoids_side:
        draw_border_side(side, trapezoid)

def draw_text(context, textbox):
    """
    Draw the given TextBox to a Cairo context from Pangocairo Context
    """
    fragment = text.TextLineFragment.from_textbox(textbox)
    context.move_to(textbox.position_x, textbox.position_y)
    context.show_layout(fragment.get_layout())

def draw_replacedbox(context, box):
    """
    Draw the given ReplacedBox to a Cairo context
    """
    x, y = box.padding_box_x(), box.padding_box_y()
    width, height = box.width, box.height
    with context.stacked():
        context.translate(x, y)
        context.rectangle(0, 0, width, height)
        context.clip()
        scale_width = width/box.replacement.intrinsic_width()
        scale_height = height/box.replacement.intrinsic_height()
        context.scale(scale_width, scale_height)
        box.replacement.draw(context)
<commit_msg>Bug fix for background-position.<commit_after># coding: utf8

#  WeasyPrint converts web documents (HTML, CSS, ...) to PDF.
#  Copyright (C) 2011  Simon Sapin
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU Affero General Public License as
#  published by the Free Software Foundation, either version 3 of the
#  License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU Affero General Public License for more details.
#
#  You should have received a copy of the GNU Affero General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import division
import urllib
try:
    from urlparse import urljoin
except ImportError:
    # Python 3
    from urllib.parse import urljoin

import cairo
import cssutils.css.value

from ..css.utils import (get_single_keyword, get_keyword,
                         get_pixel_value, get_percentage_value)
from ..formatting_structure import boxes
from .. import text
from .figures import Point, Line, Trapezoid

def get_image_surface_from_uri(uri):
    try:
        fileimage = urllib.urlopen(uri)
        if fileimage.info().gettype() != 'image/png':
            raise NotImplementedError("Only png images are implemented")
        return cairo.ImageSurface.create_from_png(fileimage)
    except IOError:
        return None

def draw_box(context, box):
    if has_background(box):
        draw_background(context, box)

    if isinstance(box, boxes.TextBox):
        draw_text(context, box)
        return

    if isinstance(box, boxes.ReplacedBox):
        draw_replacedbox(context, box)

    if isinstance(box, boxes.ParentBox):
        for child in box.children:
            draw_box(context, child)

    draw_border(context, box)

def has_background(box):
    """
    Return the given box has any background.
    """
    return box.style['background-color'][0].alpha > 0 or \
        get_single_keyword(box.style['background-image']) != 'none'

def draw_canvas_background(context, page):
    """
    Draw the canvas’s background, taken from the root element.

    If the root element is "html" and has no background, the canvas’s
    background is taken from its "body" child.

    In both cases the background position is the same as if it was drawn on
    the element.

    See http://www.w3.org/TR/CSS21/colors.html#background
    """
    if has_background(page.root_box):
        draw_background(context, page.root_box, on_entire_canvas=True)
    elif page.root_box.element.tag.lower() == 'html':
        for child in page.root_box.children:
            if child.element.tag.lower() == 'body':
                # This must be drawn now, before anything on the root element.
                draw_background(context, child, on_entire_canvas=True)

def draw_background(context, box, on_entire_canvas=False):
    """
    Draw the box background color and image to a Cairo context.
    """
    if getattr(box, 'background_drawn', False):
        return

    box.background_drawn = True

    if not has_background(box):
        return

    with context.stacked():
        # Change coordinates to make the rest easier.
        context.translate(box.border_box_x(), box.border_box_y())
        bg_width = box.border_width()
        bg_height = box.border_height()

        if not on_entire_canvas:
            context.rectangle(0, 0, bg_width, bg_height)
            context.clip()

        # Background color
        bg_color = box.style['background-color'][0]
        if bg_color.alpha > 0:
            context.set_source_colorvalue(bg_color)
            context.paint()

        # Background image
        bg_image = box.style['background-image'][0]
        if bg_image.type != 'URI':
            return

        surface = get_image_surface_from_uri(bg_image.absoluteUri)
        if not surface:
            return

        image_width = surface.get_width()
        image_height = surface.get_height()

        bg_position = box.style['background-position']
        bg_position_x, bg_position_y = absolute_background_position(
            bg_position, (bg_width, bg_height), (image_width, image_height))
        context.translate(bg_position_x, bg_position_y)

        bg_repeat = get_single_keyword(box.style['background-repeat'])
        if bg_repeat != 'repeat':
            # Get the current clip rectangle
            clip_x1, clip_y1, clip_x2, clip_y2 = context.clip_extents()
            clip_width = clip_x2 - clip_x1
            clip_height = clip_y2 - clip_y1

            if bg_repeat in ('no-repeat', 'repeat-x'):
                # Limit the drawn area vertically
                clip_y1 = 0
                clip_height = image_height

            if bg_repeat in ('no-repeat', 'repeat-y'):
                # Limit the drawn area horizontally
                clip_x1 = 0
                clip_width = image_width

            # Second clip for the background image
            context.rectangle(clip_x1, clip_y1, clip_width, clip_height)
            context.clip()

        pattern = cairo.SurfacePattern(surface)
        pattern.set_extend(cairo.EXTEND_REPEAT)
        context.set_source(pattern)
        context.paint()

def absolute_background_position(css_values, bg_dimensions, image_dimensions):
    """
    Parse values for background-position and return (position_x, position_y)
    in pixels.

    http://www.w3.org/TR/CSS21/colors.html#propdef-background-position

    :param css_values: a list of one or two cssutils Value objects.
    :param bg_dimensions: (width, height) of the background positionning area
    :param image_dimensions: (width, height) of the background image
    """
    values = list(css_values)
    keywords = [get_keyword(value) for value in values]

    if len(css_values) == 1:
        values.append(None)  # dummy value for zip()
        keywords.append('center')
    else:
        assert len(css_values) == 2

    if not (
        None in keywords or
        keywords[0] in ('left', 'right') or
        keywords[1] in ('top', 'bottom')
    ):
        values.reverse()
        keywords.reverse()
    # Order is now [horizontal, vertical]

    kw_to_percentage = dict(top=0, left=0, center=50, bottom=100, right=100)

    for value, keyword, bg_dimension, image_dimension in zip(
            values, keywords, bg_dimensions, image_dimensions):
        if keyword is not None:
            percentage = kw_to_percentage[keyword]
        else:
            percentage = get_percentage_value(value)
        if percentage is not None:
            yield (bg_dimension - image_dimension) * percentage / 100.
        else:
            yield get_pixel_value(value)

def draw_border(context, box):
    """
    Draw the box border to a Cairo context.
    """
    # TODO: implement border-spacing, border-collapse and the other border style

    def get_edge(x, y, width, height):
        return (Point(x,y), Point(x+width,y), Point(x+width, y+height),
                Point(x, y+height))

    def get_border_area():
        # border area
        x = box.position_x + box.margin_left
        y = box.position_y + box.margin_top
        border_edge = get_edge(x, y, box.border_width(), box.border_height())
        # padding area
        x = x + box.border_left_width
        y = y + box.border_top_width
        padding_edge = get_edge(x, y, box.padding_width(), box.padding_height())
        return border_edge, padding_edge

    def get_lines(rectangle):
        n = len (rectangle)
        for i, point in enumerate(rectangle):
            yield Line(rectangle[i], rectangle[(i+1) % n])

    def get_trapezoids():
        border_edge, padding_edge = get_border_area()
        for line1,line2 in zip(get_lines(border_edge), get_lines(padding_edge)):
            yield Trapezoid(line1, line2)

    def draw_border_side(side, trapezoid):
        width = float(box.style['border-%s-width'%side][0].value)
        if box.style['border-%s-width'%side] == 0:
            return
        color = box.style['border-%s-color'%side][0]
        style = box.style['border-%s-style'%side][0].value
        if color.alpha > 0:
            with context.stacked():
                if not style in ["dotted", "dashed"]:
                    trapezoid.draw_path(context)
                    context.clip()
                elif style == "dotted":
                    #TODO:Find a way to make a real dotted border
                    context.set_dash([width], 0)
                elif style == "dashed":
                    #TODO:Find a way to make a real dashed border
                    context.set_dash([4*width], 0)
                line = trapezoid.get_middle_line()
                line.draw_path(context)
                context.set_source_colorvalue(color)
                context.set_line_width(width)
                context.stroke()

    trapezoids_side = zip(["top", "right", "bottom", "left"], get_trapezoids())

    for side, trapezoid in trapezoids_side:
        draw_border_side(side, trapezoid)

def draw_text(context, textbox):
    """
    Draw the given TextBox to a Cairo context from Pangocairo Context
    """
    fragment = text.TextLineFragment.from_textbox(textbox)
    context.move_to(textbox.position_x, textbox.position_y)
    context.show_layout(fragment.get_layout())

def draw_replacedbox(context, box):
    """
    Draw the given ReplacedBox to a Cairo context
    """
    x, y = box.padding_box_x(), box.padding_box_y()
    width, height = box.width, box.height
    with context.stacked():
        context.translate(x, y)
        context.rectangle(0, 0, width, height)
        context.clip()
        scale_width = width/box.replacement.intrinsic_width()
        scale_height = height/box.replacement.intrinsic_height()
        context.scale(scale_width, scale_height)
        box.replacement.draw(context)
<|endoftext|>


<commit_before># coding: utf8

#  WeasyPrint converts web documents (HTML, CSS, ...) to PDF.
#  Copyright (C) 2011  Simon Sapin
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU Affero General Public License as
#  published by the Free Software Foundation, either version 3 of the
#  License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU Affero General Public License for more details.
#
#  You should have received a copy of the GNU Affero General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
Layout for page boxes and margin boxes.

"""

from __future__ import division

from .blocks import block_level_layout, block_level_height
from .percentages import resolve_percentages
from ..formatting_structure import boxes, build

def compute_fixed_dimension(box, outer, vertical, top_or_left):
    """
    Compute and set a margin box fixed dimension on ``box``, as described in:
    http://dev.w3.org/csswg/css3-page/#margin-constraints

    :param box:
        The margin box to work on
    :param outer:
        The target outer dimension (value of a page margin)
    :param vertical:
        True to set height, margin-top and margin-bottom; False for width,
        margin-left and margin-right
    :param top_or_left:
        True if the margin box in if the top half (for vertical==True) or
        left half (for vertical==False) of the page.
        This determines which margin should be 'auto' if the values are
        over-constrained. (Rule 3 of the algorithm.)
    """
    if vertical:
        margin_1 = box.margin_top
        margin_2 = box.margin_bottom
        inner = box.height
        padding_plus_border = (
            box.padding_top + box.padding_bottom +
            box.border_top_width + box.border_bottom_width)
    else:
        margin_1 = box.margin_left
        margin_2 = box.margin_right
        inner = box.width
        padding_plus_border = (
            box.padding_left + box.padding_right +
            box.border_left_width + box.border_right_width)

    # Rule 2
    total = padding_plus_border + sum(
        value for value in [margin_1, margin_2, inner]
        if value != 'auto')
    if total > outer:
        if margin_1 == 'auto':
            margin_1 = 0
        if margin_2 == 'auto':
            margin_2 = 0
        if inner == 'auto':
            # XXX this is not in the spec, but without it inner would end up
            # with negative value.
            # Instead, this will trigger rule 3 below.
            inner = 0
    # Rule 3
    if 'auto' not in [margin_1, margin_2, inner]:
        # Over-constrained
        if top_or_left:
            margin_1 = 'auto'
        else:
            margin_2 = 'auto'
    # Rule 4
    if [margin_1, margin_2, inner].count('auto') == 1:
        if inner == 'auto':
            inner = outer - padding_plus_border - margin_1 - margin_2
        elif margin_1 == 'auto':
            margin_1 = outer - padding_plus_border - margin_2 - inner
        elif margin_2 == 'auto':
            margin_2 = outer - padding_plus_border - margin_1 - inner
    # Rule 5
    if inner == 'auto':
        if margin_1 == 'auto':
            margin_1 = 0
        if margin_2 == 'auto':
            margin_2 = 0
        inner = outer - padding_plus_border - margin_1 - margin_2
    # Rule 6
    if margin_1 == 'auto' and margin_2 == 'auto':
        margin_1 = margin_2 = (outer - padding_plus_border - inner) / 2

    assert 'auto' not in [margin_1, margin_2, inner]
    # This should also be true, but may not be exact due to
    # floating point errors:
    #assert inner + padding_plus_border + margin_1 + margin_2 == outer
    if vertical:
        box.margin_top = margin_1
        box.margin_bottom = margin_2
        box.height = inner
    else:
        box.margin_left = margin_1
        box.margin_right = margin_2
        box.width = inner

def compute_variable_dimension(side_boxes, vertical, outer):
    """
    Compute and set a margin box fixed dimension on ``box``, as described in:
    http://dev.w3.org/csswg/css3-page/#margin-dimension

    :param side_boxes: A list of:
        - A @*-left or @*-top margin box
        - A @*-center or @*-middle margin box
        - A @*-right or @*-bottom margin box
    :param vertical:
        True to set height, margin-top and margin-bottom; False for width,
        margin-left and margin-right
    :param outer:
        The target total outer dimension (max box width or height)

    """
    box_a, box_b, box_c =  side_boxes

    padding_plus_border = 0
    for box in side_boxes:
        if vertical:
            box.margin_a = box.margin_top
            box.margin_b = box.margin_bottom
            box.inner = box.height
            if box.empty:
                # Rule 3
                box.padding_top = 0
                box.padding_bottom = 0
                box.border_top_width = 0
                box.border_bottom_width = 0
            else:
                padding_plus_border += (
                    box.padding_top + box.padding_bottom +
                    box.border_top_width + box.border_bottom_width)
        else:
            box.margin_a = box.margin_left
            box.margin_b = box.margin_right
            box.inner = box.width
            if box.empty:
                # Rule 3
                box.padding_left = 0
                box.padding_right = 0
                box.border_left_width = 0
                box.border_right_width = 0
            else:
                padding_plus_border += (
                    box.padding_left + box.padding_right +
                    box.border_left_width + box.border_right_width)

    total = padding_plus_border + sum(
        value
        for box in side_boxes
        for value in [box.margin_a, box.margin_b, box.inner]
        if value != 'auto')

    if box_b.empty:
        # TODO: rule 2
        # box_a.outer() == box_b.outer()
        pass

    # TODO: Actual layout

    for box in side_boxes:
        if vertical:
            box.margin_top = box.margin_a
            box.margin_bottom = box.margin_a
            box.height = box.inner
        else:
            box.margin_left = box.margin_a
            box.margin_right = box.margin_a
            box.width = box.inner
        del box.margin_a
        del box.margin_b
        del box.inner

    # XXX
    if vertical:
        names = ['height', 'margin_top', 'margin_bottom']
    else:
        names = ['width', 'margin_left', 'margin_right']
    for box in [box_a, box_b, box_c]:
        if box is not None:
            for name in names:
                if getattr(box, name) == 'auto':
                    setattr(box, name, 0)

def empty_margin_boxes(document, page, page_type):
    """
    Return the margin boxes for this page that have their own dimensions and
    position set, but still need their content.

    Parameters: same as :func:`make_margin_boxes`.

    """
    # This is a closure only to make calls shorter
    def make_box(at_keyword, containing_block):
        """
        Return a margin box with resolved percentages, but that may still
        have 'auto' values.

        Return ``None`` if this margin box should not be generated.

        :param at_keyword: which margin box to return, eg. '@top-left'
        :param containing_block: as expected by :func:`resolve_percentages`.

        """
        style = document.style_for(page_type, at_keyword)
        if style is None:
            style = page.style.inherit_from()
        box = boxes.MarginBox(at_keyword, style)
        resolve_percentages(box, containing_block)
        # Empty boxes should not be generated, but they may be needed for
        # the layout of their neighbors.
        box.empty = (style.content in ('normal', 'none') and
                     style.width == 'auto')
        return box

    margin_top = page.margin_top
    margin_bottom = page.margin_bottom
    margin_left = page.margin_left
    margin_right = page.margin_right
    max_box_width = page.border_width()
    max_box_height = page.border_height()

    # bottom right corner of the border box
    page_end_x = margin_left + max_box_width
    page_end_y = margin_top + max_box_height

    # Margin box dimensions, described in
    # http://dev.w3.org/csswg/css3-page/#margin-box-dimensions

    # Order recommended on http://dev.w3.org/csswg/css3-page/#painting
    # center/middle on top (last in tree order), then corner, then others

    # First, boxes that are neither corner nor center/middle
    # Delay center/middle boxes
    generated_boxes = []
    delayed_boxes = []

    for prefix, vertical, containing_block, position_x, position_y in [
        ('top', False, (max_box_width, margin_top),
            margin_left, 0),
        ('bottom', False, (max_box_width, margin_bottom),
            margin_left, page_end_y),
        ('left', True, (margin_left, max_box_height),
            0, margin_top),
        ('right', True, (margin_right, max_box_height),
            page_end_x, margin_top),
    ]:
        if vertical:
            suffixes = ['top', 'middle', 'bottom']
            fixed_outer, variable_outer = containing_block
        else:
            suffixes = ['left', 'center', 'right']
            variable_outer, fixed_outer = containing_block
        side_boxes = [make_box('@%s-%s' % (prefix, suffix), containing_block)
                      for suffix in suffixes]
        # We need the three boxes together for the variable dimension:
        compute_variable_dimension(side_boxes, vertical, variable_outer)
        for box, delay in zip(side_boxes, [False, True, False]):
            if box is not None:
                compute_fixed_dimension(
                    box, fixed_outer, not vertical, prefix in ['top', 'left'])
                box.position_x = position_x
                box.position_y = position_y
                if vertical:
                    position_y += box.margin_height()
                else:
                    position_x += box.margin_width()
                if delay:
                    delayed_boxes.append(box)
                else:
                    generated_boxes.append(box)

    # Corner boxes

    for at_keyword, cb_width, cb_height, position_x, position_y in [
        ('@top-left-corner', margin_left, margin_top, 0, 0),
        ('@top-right-corner', margin_right, margin_top, page_end_x, 0),
        ('@bottom-left-corner', margin_left, margin_bottom, 0, page_end_y),
        ('@bottom-right-corner', margin_right, margin_bottom,
            page_end_x, page_end_y),
    ]:
        box = make_box(at_keyword, (cb_width, cb_height))
        if box is not None:
            box.position_x = position_x
            box.position_y = position_y
            compute_fixed_dimension(box, cb_height, True, 'top' in at_keyword)
            compute_fixed_dimension(box, cb_width, False, 'left' in at_keyword)
            generated_boxes.append(box)

    generated_boxes.extend(delayed_boxes)
    return generated_boxes

def make_margin_boxes(document, page, page_type):
    """Yield laid-out margin boxes for this page.

    :param document: a :class:`Document` object
    :param page: a :class:`PageBox` object
    :param page_type: as returned by :func:`page_type_for_number`

    """
    for box in empty_margin_boxes(document, page, page_type):
        if box.empty:
            continue
        # TODO: get actual counter values at the time of the last page break
        counter_values = {}
        quote_depth = [0]
        children = build.content_to_boxes(
            document, box.style, box, quote_depth, counter_values)
        box = box.copy_with_children(children)
        # content_to_boxes() only produces inline-level boxes
        box = build.inline_in_block(box)
        box, resume_at = block_level_height(document, box,
            max_position_y=float('inf'), skip_stack=None,
            device_size=page.style.size, page_is_empty=True)
        assert resume_at is None
        yield box

def page_type_for_number(page_number):
    """Return a page type such as ``'first_right_page'`` from a page number."""
    # First page is a right page.
    # TODO: this "should depend on the major writing direction of the
    # document".
    first_is_right = True

    is_right = (page_number % 2) == (1 if first_is_right else 0)
    page_type = 'right_page' if is_right else 'left_page'
    if page_number == 1:
        page_type = 'first_' + page_type
    return page_type

def make_page(document, page_number, resume_at):
    """Take just enough content from the beginning to fill one page.

    Return ``(page, finished)``. ``page`` is a laid out PageBox object
    and ``resume_at`` indicates where in the document to start the next page,
    or is ``None`` if this was the last page.

    :param document: a Document object
    :param page_number: integer, start at 1 for the first page
    :param resume_at: as returned by ``make_page()`` for the previous page,
                      or ``None`` for the first page.

    """
    root_box = document.formatting_structure
    page_type = page_type_for_number(page_number)
    style = document.style_for(page_type)
    page = boxes.PageBox(page_number, style, root_box.style.direction)

    device_size = page.style.size
    page.outer_width, page.outer_height = device_size

    resolve_percentages(page, device_size)

    page.position_x = 0
    page.position_y = 0
    page.width = page.outer_width - page.horizontal_surroundings()
    page.height = page.outer_height - page.vertical_surroundings()

    root_box.position_x = page.content_box_x()
    root_box.position_y = page.content_box_y()
    page_content_bottom = root_box.position_y + page.height
    initial_containing_block = page

    # TODO: handle cases where the root element is something else.
    # See http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
    assert isinstance(root_box, boxes.BlockBox)
    root_box, resume_at = block_level_layout(
        document, root_box, page_content_bottom, resume_at,
        initial_containing_block, device_size, page_is_empty=True)
    assert root_box

    children = [root_box]
    children.extend(make_margin_boxes(document, page, page_type))

    page = page.copy_with_children(children)

    return page, resume_at
<commit_msg>Avoid layout work for boxes that end up not generated.<commit_after># coding: utf8

#  WeasyPrint converts web documents (HTML, CSS, ...) to PDF.
#  Copyright (C) 2011  Simon Sapin
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU Affero General Public License as
#  published by the Free Software Foundation, either version 3 of the
#  License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU Affero General Public License for more details.
#
#  You should have received a copy of the GNU Affero General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
Layout for page boxes and margin boxes.

"""

from __future__ import division

from .blocks import block_level_layout, block_level_height
from .percentages import resolve_percentages
from ..formatting_structure import boxes, build

def compute_fixed_dimension(box, outer, vertical, top_or_left):
    """
    Compute and set a margin box fixed dimension on ``box``, as described in:
    http://dev.w3.org/csswg/css3-page/#margin-constraints

    :param box:
        The margin box to work on
    :param outer:
        The target outer dimension (value of a page margin)
    :param vertical:
        True to set height, margin-top and margin-bottom; False for width,
        margin-left and margin-right
    :param top_or_left:
        True if the margin box in if the top half (for vertical==True) or
        left half (for vertical==False) of the page.
        This determines which margin should be 'auto' if the values are
        over-constrained. (Rule 3 of the algorithm.)
    """
    if vertical:
        margin_1 = box.margin_top
        margin_2 = box.margin_bottom
        inner = box.height
        padding_plus_border = (
            box.padding_top + box.padding_bottom +
            box.border_top_width + box.border_bottom_width)
    else:
        margin_1 = box.margin_left
        margin_2 = box.margin_right
        inner = box.width
        padding_plus_border = (
            box.padding_left + box.padding_right +
            box.border_left_width + box.border_right_width)

    # Rule 2
    total = padding_plus_border + sum(
        value for value in [margin_1, margin_2, inner]
        if value != 'auto')
    if total > outer:
        if margin_1 == 'auto':
            margin_1 = 0
        if margin_2 == 'auto':
            margin_2 = 0
        if inner == 'auto':
            # XXX this is not in the spec, but without it inner would end up
            # with negative value.
            # Instead, this will trigger rule 3 below.
            inner = 0
    # Rule 3
    if 'auto' not in [margin_1, margin_2, inner]:
        # Over-constrained
        if top_or_left:
            margin_1 = 'auto'
        else:
            margin_2 = 'auto'
    # Rule 4
    if [margin_1, margin_2, inner].count('auto') == 1:
        if inner == 'auto':
            inner = outer - padding_plus_border - margin_1 - margin_2
        elif margin_1 == 'auto':
            margin_1 = outer - padding_plus_border - margin_2 - inner
        elif margin_2 == 'auto':
            margin_2 = outer - padding_plus_border - margin_1 - inner
    # Rule 5
    if inner == 'auto':
        if margin_1 == 'auto':
            margin_1 = 0
        if margin_2 == 'auto':
            margin_2 = 0
        inner = outer - padding_plus_border - margin_1 - margin_2
    # Rule 6
    if margin_1 == 'auto' and margin_2 == 'auto':
        margin_1 = margin_2 = (outer - padding_plus_border - inner) / 2

    assert 'auto' not in [margin_1, margin_2, inner]
    # This should also be true, but may not be exact due to
    # floating point errors:
    #assert inner + padding_plus_border + margin_1 + margin_2 == outer
    if vertical:
        box.margin_top = margin_1
        box.margin_bottom = margin_2
        box.height = inner
    else:
        box.margin_left = margin_1
        box.margin_right = margin_2
        box.width = inner

def compute_variable_dimension(side_boxes, vertical, outer):
    """
    Compute and set a margin box fixed dimension on ``box``, as described in:
    http://dev.w3.org/csswg/css3-page/#margin-dimension

    :param side_boxes: A list of:
        - A @*-left or @*-top margin box
        - A @*-center or @*-middle margin box
        - A @*-right or @*-bottom margin box
    :param vertical:
        True to set height, margin-top and margin-bottom; False for width,
        margin-left and margin-right
    :param outer:
        The target total outer dimension (max box width or height)

    """
    box_a, box_b, box_c =  side_boxes

    padding_plus_border = 0
    for box in side_boxes:
        if vertical:
            box.margin_a = box.margin_top
            box.margin_b = box.margin_bottom
            box.inner = box.height
            if box.empty:
                # Rule 3
                box.padding_top = 0
                box.padding_bottom = 0
                box.border_top_width = 0
                box.border_bottom_width = 0
            else:
                padding_plus_border += (
                    box.padding_top + box.padding_bottom +
                    box.border_top_width + box.border_bottom_width)
        else:
            box.margin_a = box.margin_left
            box.margin_b = box.margin_right
            box.inner = box.width
            if box.empty:
                # Rule 3
                box.padding_left = 0
                box.padding_right = 0
                box.border_left_width = 0
                box.border_right_width = 0
            else:
                padding_plus_border += (
                    box.padding_left + box.padding_right +
                    box.border_left_width + box.border_right_width)

    total = padding_plus_border + sum(
        value
        for box in side_boxes
        for value in [box.margin_a, box.margin_b, box.inner]
        if value != 'auto')

    if box_b.empty:
        # TODO: rule 2
        # box_a.outer() == box_b.outer()
        pass

    # TODO: Actual layout

    for box in side_boxes:
        if vertical:
            box.margin_top = box.margin_a
            box.margin_bottom = box.margin_a
            box.height = box.inner
        else:
            box.margin_left = box.margin_a
            box.margin_right = box.margin_a
            box.width = box.inner
        del box.margin_a
        del box.margin_b
        del box.inner

    # XXX
    if vertical:
        names = ['height', 'margin_top', 'margin_bottom']
    else:
        names = ['width', 'margin_left', 'margin_right']
    for box in [box_a, box_b, box_c]:
        if box is not None:
            for name in names:
                if getattr(box, name) == 'auto':
                    setattr(box, name, 0)

def empty_margin_boxes(document, page, page_type):
    """
    Return the margin boxes for this page that have their own dimensions and
    position set, but still need their content.

    Parameters: same as :func:`make_margin_boxes`.

    """
    # This is a closure only to make calls shorter
    def make_box(at_keyword, containing_block):
        """
        Return a margin box with resolved percentages, but that may still
        have 'auto' values.

        Return ``None`` if this margin box should not be generated.

        :param at_keyword: which margin box to return, eg. '@top-left'
        :param containing_block: as expected by :func:`resolve_percentages`.

        """
        style = document.style_for(page_type, at_keyword)
        if style is None:
            style = page.style.inherit_from()
        box = boxes.MarginBox(at_keyword, style)
        resolve_percentages(box, containing_block)
        # Empty boxes should not be generated, but they may be needed for
        # the layout of their neighbors.
        box.empty = (style.content in ('normal', 'none') and
                     style.width == 'auto')
        return box

    margin_top = page.margin_top
    margin_bottom = page.margin_bottom
    margin_left = page.margin_left
    margin_right = page.margin_right
    max_box_width = page.border_width()
    max_box_height = page.border_height()

    # bottom right corner of the border box
    page_end_x = margin_left + max_box_width
    page_end_y = margin_top + max_box_height

    # Margin box dimensions, described in
    # http://dev.w3.org/csswg/css3-page/#margin-box-dimensions

    # Order recommended on http://dev.w3.org/csswg/css3-page/#painting
    # center/middle on top (last in tree order), then corner, then others

    # First, boxes that are neither corner nor center/middle
    # Delay center/middle boxes
    generated_boxes = []
    delayed_boxes = []

    for prefix, vertical, containing_block, position_x, position_y in [
        ('top', False, (max_box_width, margin_top),
            margin_left, 0),
        ('bottom', False, (max_box_width, margin_bottom),
            margin_left, page_end_y),
        ('left', True, (margin_left, max_box_height),
            0, margin_top),
        ('right', True, (margin_right, max_box_height),
            page_end_x, margin_top),
    ]:
        if vertical:
            suffixes = ['top', 'middle', 'bottom']
            fixed_outer, variable_outer = containing_block
        else:
            suffixes = ['left', 'center', 'right']
            variable_outer, fixed_outer = containing_block
        side_boxes = [make_box('@%s-%s' % (prefix, suffix), containing_block)
                      for suffix in suffixes]
        if all(box.empty for box in side_boxes):
            continue
        # We need the three boxes together for the variable dimension:
        compute_variable_dimension(side_boxes, vertical, variable_outer)
        for box, delay in zip(side_boxes, [False, True, False]):
            if box.empty:
                continue
            compute_fixed_dimension(
                box, fixed_outer, not vertical, prefix in ['top', 'left'])
            box.position_x = position_x
            box.position_y = position_y
            if vertical:
                position_y += box.margin_height()
            else:
                position_x += box.margin_width()
            if delay:
                delayed_boxes.append(box)
            else:
                generated_boxes.append(box)

    # Corner boxes

    for at_keyword, cb_width, cb_height, position_x, position_y in [
        ('@top-left-corner', margin_left, margin_top, 0, 0),
        ('@top-right-corner', margin_right, margin_top, page_end_x, 0),
        ('@bottom-left-corner', margin_left, margin_bottom, 0, page_end_y),
        ('@bottom-right-corner', margin_right, margin_bottom,
            page_end_x, page_end_y),
    ]:
        box = make_box(at_keyword, (cb_width, cb_height))
        if box.empty:
            continue
        box.position_x = position_x
        box.position_y = position_y
        compute_fixed_dimension(box, cb_height, True, 'top' in at_keyword)
        compute_fixed_dimension(box, cb_width, False, 'left' in at_keyword)
        generated_boxes.append(box)

    generated_boxes.extend(delayed_boxes)
    return generated_boxes

def make_margin_boxes(document, page, page_type):
    """Yield laid-out margin boxes for this page.

    :param document: a :class:`Document` object
    :param page: a :class:`PageBox` object
    :param page_type: as returned by :func:`page_type_for_number`

    """
    for box in empty_margin_boxes(document, page, page_type):
        if box.style.content in ('normal', 'none'):
            continue
        # TODO: get actual counter values at the time of the last page break
        counter_values = {}
        quote_depth = [0]
        children = build.content_to_boxes(
            document, box.style, box, quote_depth, counter_values)
        box = box.copy_with_children(children)
        # content_to_boxes() only produces inline-level boxes
        box = build.inline_in_block(box)
        box, resume_at = block_level_height(document, box,
            max_position_y=float('inf'), skip_stack=None,
            device_size=page.style.size, page_is_empty=True)
        assert resume_at is None
        yield box

def page_type_for_number(page_number):
    """Return a page type such as ``'first_right_page'`` from a page number."""
    # First page is a right page.
    # TODO: this "should depend on the major writing direction of the
    # document".
    first_is_right = True

    is_right = (page_number % 2) == (1 if first_is_right else 0)
    page_type = 'right_page' if is_right else 'left_page'
    if page_number == 1:
        page_type = 'first_' + page_type
    return page_type

def make_page(document, page_number, resume_at):
    """Take just enough content from the beginning to fill one page.

    Return ``(page, finished)``. ``page`` is a laid out PageBox object
    and ``resume_at`` indicates where in the document to start the next page,
    or is ``None`` if this was the last page.

    :param document: a Document object
    :param page_number: integer, start at 1 for the first page
    :param resume_at: as returned by ``make_page()`` for the previous page,
                      or ``None`` for the first page.

    """
    root_box = document.formatting_structure
    page_type = page_type_for_number(page_number)
    style = document.style_for(page_type)
    page = boxes.PageBox(page_number, style, root_box.style.direction)

    device_size = page.style.size
    page.outer_width, page.outer_height = device_size

    resolve_percentages(page, device_size)

    page.position_x = 0
    page.position_y = 0
    page.width = page.outer_width - page.horizontal_surroundings()
    page.height = page.outer_height - page.vertical_surroundings()

    root_box.position_x = page.content_box_x()
    root_box.position_y = page.content_box_y()
    page_content_bottom = root_box.position_y + page.height
    initial_containing_block = page

    # TODO: handle cases where the root element is something else.
    # See http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
    assert isinstance(root_box, boxes.BlockBox)
    root_box, resume_at = block_level_layout(
        document, root_box, page_content_bottom, resume_at,
        initial_containing_block, device_size, page_is_empty=True)
    assert root_box

    children = [root_box]
    children.extend(make_margin_boxes(document, page, page_type))

    page = page.copy_with_children(children)

    return page, resume_at
<|endoftext|>